Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Box Plot visualization to React #3948

Merged
merged 7 commits into from
Jul 4, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/app/pages/dashboards/dashboard.less
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
.sankey-visualization-container,
.map-visualization-container,
.word-cloud-visualization-container,
.box-plot-deprecated-visualization-container,
.plotly-chart-container {
position: absolute;
left: 0;
Expand Down
19 changes: 2 additions & 17 deletions client/app/services/resizeObserver.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
/* global ResizeObserver */

function observeNative(node, callback) {
if ((typeof ResizeObserver === 'function') && node) {
const observer = new ResizeObserver(() => callback()); // eslint-disable-line compat/compat
observer.observe(node);
return () => observer.disconnect();
}
return null;
}

const items = new Map();

function checkItems() {
Expand All @@ -33,7 +22,7 @@ function checkItems() {
}
}

function observeFallback(node, callback) {
export default function observe(node, callback) {
if (node && !items.has(node)) {
const shouldTrigger = items.size === 0;
items.set(node, { callback });
Expand All @@ -42,9 +31,5 @@ function observeFallback(node, callback) {
}
return () => items.delete(node);
}
return null;
}

export default function observe(node, callback) {
return observeNative(node, callback) || observeFallback(node, callback) || (() => {});
return () => {};
}
2 changes: 2 additions & 0 deletions client/app/visualizations/box-plot/Editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default function Editor({ options, onOptionsChange }) {
<div className="form-group">
<label className="control-label" htmlFor="box-plot-x-axis-label">X Axis Label</label>
<Input
data-test="BoxPlot.XAxisLabel"
id="box-plot-x-axis-label"
value={options.xAxisLabel}
onChange={event => onXAxisLabelChanged(event.target.value)}
Expand All @@ -27,6 +28,7 @@ export default function Editor({ options, onOptionsChange }) {
<div className="form-group">
<label className="control-label" htmlFor="box-plot-y-axis-label">Y Axis Label</label>
<Input
data-test="BoxPlot.YAxisLabel"
id="box-plot-y-axis-label"
value={options.yAxisLabel}
onChange={event => onYAxisLabelChanged(event.target.value)}
Expand Down
176 changes: 176 additions & 0 deletions client/app/visualizations/box-plot/Renderer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { map, each } from 'lodash';
import d3 from 'd3';
import React, { useState, useEffect } from 'react';
import resizeObserver from '@/services/resizeObserver';
import { RendererPropTypes } from '@/visualizations';
import box from './d3box';
import './renderer.less';

function calcIqr(k) {
return (d) => {
const q1 = d.quartiles[0];
const q3 = d.quartiles[2];
const iqr = (q3 - q1) * k;

let i = -1;
let j = d.length;

i += 1;
while (d[i] < q1 - iqr) {
i += 1;
}

j -= 1;
while (d[j] > q3 + iqr) {
j -= 1;
}

return [i, j];
};
}

function render(container, data, { xAxisLabel, yAxisLabel }) {
container = d3.select(container);

const containerBounds = container.node().getBoundingClientRect();
const containerWidth = Math.floor(containerBounds.width);
const containerHeight = Math.floor(containerBounds.height);

const margin = {
top: 10, right: 50, bottom: 40, left: 50, inner: 25,
};
const width = containerWidth - margin.right - margin.left;
const height = containerHeight - margin.top - margin.bottom;

let min = Infinity;
let max = -Infinity;
const mydata = [];
let value = 0;
let d = [];

const columns = map(data.columns, col => col.name);
const xscale = d3.scale.ordinal()
.domain(columns)
.rangeBands([0, containerWidth - margin.left - margin.right]);

let boxWidth;
if (columns.length > 1) {
boxWidth = Math.min(xscale(columns[1]), 120.0);
} else {
boxWidth = 120.0;
}
margin.inner = boxWidth / 3.0;

each(columns, (column, i) => {
d = mydata[i] = [];
each(data.rows, (row) => {
value = row[column];
d.push(value);
if (value > max) max = Math.ceil(value);
if (value < min) min = Math.floor(value);
});
});

const yscale = d3.scale.linear()
.domain([min * 0.99, max * 1.01])
.range([height, 0]);

const chart = box()
.whiskers(calcIqr(1.5))
.width(boxWidth - 2 * margin.inner)
.height(height)
.domain([min * 0.99, max * 1.01]);
const xAxis = d3.svg.axis()
.scale(xscale)
.orient('bottom');


const yAxis = d3.svg.axis()
.scale(yscale)
.orient('left');

const xLines = d3.svg.axis()
.scale(xscale)
.tickSize(height)
.orient('bottom');

const yLines = d3.svg.axis()
.scale(yscale)
.tickSize(width)
.orient('right');

function barOffset(i) {
return xscale(columns[i]) + (xscale(columns[1]) - margin.inner) / 2.0;
}

container.selectAll('*').remove();

const svg = container.append('svg')
.attr('width', containerWidth)
.attr('height', height + margin.bottom + margin.top);

const plot = svg.append('g')
.attr('width', containerWidth - margin.left - margin.right)
.attr('transform', `translate(${margin.left},${margin.top})`);

svg.append('text')
.attr('class', 'box')
.attr('x', containerWidth / 2.0)
.attr('text-anchor', 'middle')
.attr('y', height + margin.bottom)
.text(xAxisLabel);

svg.append('text')
.attr('class', 'box')
.attr('transform', `translate(10,${(height + margin.top + margin.bottom) / 2.0})rotate(-90)`)
.attr('text-anchor', 'middle')
.text(yAxisLabel);

plot.append('rect')
.attr('class', 'grid-background')
.attr('width', width)
.attr('height', height);

plot.append('g')
.attr('class', 'grid')
.call(yLines);

plot.append('g')
.attr('class', 'grid')
.call(xLines);

plot.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0,${height})`)
.call(xAxis);

plot.append('g')
.attr('class', 'y axis')
.call(yAxis);

plot.selectAll('.box').data(mydata)
.enter().append('g')
.attr('class', 'box')
.attr('width', boxWidth)
.attr('height', height)
.attr('transform', (_, i) => `translate(${barOffset(i)},${0})`)
.call(chart);
}

export default function Renderer({ data, options }) {
const [container, setContainer] = useState(null);

useEffect(() => {
if (container) {
render(container, data, options);
const unwatch = resizeObserver(container, () => {
render(container, data, options);
});
return unwatch;
}
}, [container, data, options]);

return (<div className="box-plot-deprecated-visualization-container" ref={setContainer} />);
}

Renderer.propTypes = RendererPropTypes;
Loading