From 6c7bd9906b4ead3569f6a6cb1f0851beea78b447 Mon Sep 17 00:00:00 2001 From: Allen Short Date: Thu, 31 May 2018 10:47:34 -0500 Subject: [PATCH] plotly chart component --- client/app/react-components/PlotlyChart.js | 114 ++++++++++++++++++ .../app/visualizations/chart/plotly/index.js | 72 +---------- .../app/visualizations/chart/plotly/utils.js | 2 - package.json | 4 +- 4 files changed, 121 insertions(+), 71 deletions(-) create mode 100644 client/app/react-components/PlotlyChart.js diff --git a/client/app/react-components/PlotlyChart.js b/client/app/react-components/PlotlyChart.js new file mode 100644 index 0000000000..ce6139e128 --- /dev/null +++ b/client/app/react-components/PlotlyChart.js @@ -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) => { + 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); + } + + 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}`); + } + } + } + + 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 ( + + ); + } +} diff --git a/client/app/visualizations/chart/plotly/index.js b/client/app/visualizations/chart/plotly/index.js index 0cbd9a416a..a1d3a5c9e4 100644 --- a/client/app/visualizations/chart/plotly/index.js +++ b/client/app/visualizations/chart/plotly/index.js @@ -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'; @@ -21,67 +18,6 @@ Plotly.setPlotConfig({ modeBarButtonsToRemove: ['sendDataToCloud'], }); -const PlotlyChart = () => ({ - restrict: 'E', - template: '
', - 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: '
', @@ -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); } diff --git a/client/app/visualizations/chart/plotly/utils.js b/client/app/visualizations/chart/plotly/utils.js index b329059397..c638506bb3 100644 --- a/client/app/visualizations/chart/plotly/utils.js +++ b/client/app/visualizations/chart/plotly/utils.js @@ -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, }; diff --git a/package.json b/package.json index ee69d2e41b..c420759fdb 100644 --- a/package.json +++ b/package.json @@ -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"