Skip to content

Commit

Permalink
Migrate Box Plot visualization to React (getredash#3948)
Browse files Browse the repository at this point in the history
  • Loading branch information
kravets-levko authored and harveyrendell committed Nov 14, 2019
1 parent b99e394 commit d46cfaa
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 200 deletions.
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;
File renamed without changes.
Loading

0 comments on commit d46cfaa

Please sign in to comment.