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

React component for plotly charts #2633

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
114 changes: 114 additions & 0 deletions client/app/react-components/PlotlyChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from 'react';
import PropTypes from 'prop-types';
import createPlotlyComponent from 'react-plotly.js/factory';
import Plotly from 'plotly.js';
import bar from 'plotly.js/lib/bar';
import pie from 'plotly.js/lib/pie';
import histogram from 'plotly.js/lib/histogram';
import box from 'plotly.js/lib/box';
import { each, isArray, isObject } from 'lodash';
import { normalizeValue, updateData, prepareData, prepareLayout } from '@/visualizations/chart/plotly/utils';


Plotly.register([bar, pie, histogram, box]);
Plotly.setPlotConfig({
modeBarButtonsToRemove: ['sendDataToCloud'],
});

const Plot = createPlotlyComponent(Plotly);


const timeSeriesToPlotlySeries = (ss) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW the parens are optional when there is only one variable. This could be rewritten as:

const timeSeriesToPlotlySeries = ss => {

There are also many places below where this comment applies, like line 24 and line 26. But it's obviously just style, and it's up to you.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious. eslint has caught this many times for me, not sure how I missed this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-- Ah. the arrow-parens eslint rule expects parens for braceful arrow functions and no parens for braceless ones.

const x = [];
const ys = {};
each(ss, (series) => {
ys[series.name] = [];
each(series.data, (point) => {
x.push(normalizeValue(point.x));
ys[series.name].push(normalizeValue(point.y));
});
});
return [x, ys];
};

export default class PlotlyChart extends React.Component {
static propTypes = {
// XXX make this required after porting next layer up
options: PropTypes.object,
// eslint-disable-next-line react/no-unused-prop-types
series: PropTypes.array.isRequired,
customCode: PropTypes.string,

}

static defaultProps = { options: null, customCode: null };

constructor(props) {
super(props);
this.state = {
data: null,
layout: null,
revision: 0,
x: null,
ys: null,
};
this.refreshCustom = this.refreshCustom.bind(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a neat way around this that I recently discovered myself. You can omit this line if you define refreshCustom like this:

refreshCustom = (figure, plotlyElement) => {
  // ...
}

See this blog post section for more info. As with everything, there are pros and cons with this approach, but if you find that you have a lot of this.whatever = this.whatever.bind(this) lines in your code, it's a trick worth considering.

}

static getDerivedStateFromProps(nextProps, prevState) {
if (!nextProps.options) return null;
if (nextProps.options.globalSeriesType === 'custom') {
const [x, ys] = timeSeriesToPlotlySeries(nextProps.series);
return { x, ys, revision: prevState.revision + 1 };
}
const data = prepareData(nextProps.series, nextProps.options);
updateData(data, nextProps.options);
return {
data,
layout: prepareLayout(null, nextProps.series, nextProps.options, data),
revision: prevState.revision + 1,
};
}

refreshCustom = (figure, plotlyElement) => {
Plotly.newPlot(plotlyElement);
try {
// eslint-disable-next-line no-new-func
const codeCall = new Function('x, ys, element, Plotly', this.props.options.customCode);
codeCall(this.state.x, this.state.ys, plotlyElement, Plotly);
} catch (err) {
if (this.props.options.enableConsoleLogs) {
// eslint-disable-next-line no-console
console.log(`Error while executing custom graph: ${err}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of template strings! 😃

}
}
}

restyle = (updates) => {
if (isArray(updates) && isObject(updates[0]) && updates[0].visible) {
updateData(this.state.data, this.props.options);
this.setState({ revision: this.state.revision + 1 });
}
}

render() {
if (!this.props.options) return null;
return (
<Plot
className="plotly-chart-container"
revision={this.state.revision}
style={{ width: '100%', height: '100%' }}
useResizeHandler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, I didn't even know you could do this. Nice find!

config={{
showLink: false,
displayLogo: false,
modeBarButtonsToRemove: ['sendDataToCloud'],
}}
data={this.state.data}
layout={this.state.layout}
onRestyle={this.restyle}
onUpdate={this.props.options.globalSeriesType === 'custom' ? this.refreshCustom : null}
/>
);
}
}
72 changes: 4 additions & 68 deletions client/app/visualizations/chart/plotly/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { each, debounce, isArray, isObject } from 'lodash';
import { each } from 'lodash';

import Plotly from 'plotly.js/lib/core';
import bar from 'plotly.js/lib/bar';
import pie from 'plotly.js/lib/pie';
import histogram from 'plotly.js/lib/histogram';
import box from 'plotly.js/lib/box';
import { react2angular } from 'react2angular';

import PlotlyChart from '@/react-components/PlotlyChart';
import {
ColorPalette,
prepareData,
prepareLayout,
calculateMargins,
updateDimensions,
updateData,
normalizeValue,
} from './utils';

Expand All @@ -21,67 +18,6 @@ Plotly.setPlotConfig({
modeBarButtonsToRemove: ['sendDataToCloud'],
});

const PlotlyChart = () => ({
restrict: 'E',
template: '<div class="plotly-chart-container" resize-event="handleResize()"></div>',
scope: {
options: '=',
series: '=',
},
link(scope, element) {
const plotlyElement = element[0].querySelector('.plotly-chart-container');
const plotlyOptions = { showLink: false, displaylogo: false };
let layout = {};
let data = [];

const updateChartDimensions = () => {
if (updateDimensions(layout, plotlyElement, calculateMargins(plotlyElement))) {
Plotly.relayout(plotlyElement, layout);
}
};

function update() {
if (['normal', 'percent'].indexOf(scope.options.series.stacking) >= 0) {
// Backward compatibility
scope.options.series.percentValues = scope.options.series.stacking === 'percent';
scope.options.series.stacking = 'stack';
}

data = prepareData(scope.series, scope.options);
updateData(data, scope.options);
layout = prepareLayout(plotlyElement, scope.series, scope.options, data);

// It will auto-purge previous graph
Plotly.newPlot(plotlyElement, data, layout, plotlyOptions);

plotlyElement.on('plotly_restyle', (updates) => {
// This event is triggered if some plotly data/layout has changed.
// We need to catch only changes of traces visibility to update stacking
if (isArray(updates) && isObject(updates[0]) && updates[0].visible) {
updateData(data, scope.options);
Plotly.relayout(plotlyElement, layout);
}
});

plotlyElement.on('plotly_afterplot', updateChartDimensions);
}
update();

scope.$watch('series', (oldValue, newValue) => {
if (oldValue !== newValue) {
update();
}
});
scope.$watch('options', (oldValue, newValue) => {
if (oldValue !== newValue) {
update();
}
}, true);

scope.handleResize = debounce(updateChartDimensions, 50);
},
});

const CustomPlotlyChart = clientConfig => ({
restrict: 'E',
template: '<div class="plotly-chart-container" resize-event="handleResize()"></div>',
Expand Down Expand Up @@ -139,6 +75,6 @@ const CustomPlotlyChart = clientConfig => ({

export default function init(ngModule) {
ngModule.constant('ColorPalette', ColorPalette);
ngModule.directive('plotlyChart', PlotlyChart);
ngModule.component('plotlyChart', react2angular(PlotlyChart));
ngModule.directive('customPlotlyChart', CustomPlotlyChart);
}
2 changes: 0 additions & 2 deletions client/app/visualizations/chart/plotly/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,6 @@ export function prepareLayout(element, seriesList, options, data) {
t: 20,
pad: 4,
},
width: Math.floor(element.offsetWidth),
height: Math.floor(element.offsetHeight),
autosize: true,
showlegend: has(options, 'legend') ? options.legend.enabled : true,
};
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@
"numeral": "^2.0.6",
"pace-progress": "git+https://github.com/getredash/pace.git",
"pivottable": "^2.15.0",
"plotly.js": "1.30.1",
"plotly.js": "^1.37.1",
"prop-types": "^15.6.1",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-plotly.js": "^2.2.0",
"react-select": "^1.2.1",
"react2angular": "^3.2.1",
"ui-select": "^0.19.8",
"underscore.string": "^3.3.4"
Expand Down