From 37af4f5151558ffb8261e5e14f0473faca6dbd26 Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Wed, 26 Jul 2017 23:10:29 +0200 Subject: [PATCH] Move plot to raw d3 with react Remove rd3 dependency for plots. Drawing with react and raw d3 drastically improves performance. This is most due d3 has its own DOM manipulation. If this is used, it interferes with reacts DOM manipulation. Now only d3 helper libraries are used and the svg is drawn by react itself. Thus the performance gets a huge boost on plots. --- package.json | 6 +- src/components/widget-plot.js | 26 ++--- src/components/widget-plot/plot.js | 158 +++++++---------------------- 3 files changed, 53 insertions(+), 137 deletions(-) diff --git a/package.json b/package.json index 98e3fa3..e71b043 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,11 @@ "dependencies": { "bootstrap": "^3.3.7", "classnames": "^2.2.5", - "d3-scale": "^1.0.5", + "d3-array": "^1.2.0", + "d3-axis": "^1.0.8", + "d3-scale": "^1.0.6", + "d3-selection": "^1.1.0", + "d3-shape": "^1.2.0", "es6-promise": "^4.0.5", "flux": "^3.1.2", "gaugeJS": "^1.3.2", diff --git a/src/components/widget-plot.js b/src/components/widget-plot.js index c02aa97..56b5b2a 100644 --- a/src/components/widget-plot.js +++ b/src/components/widget-plot.js @@ -19,13 +19,12 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import React, { Component } from 'react'; +import React from 'react'; import Plot from './widget-plot/plot'; -import PlotLegend from './widget-plot/plot-legend'; - -class WidgetPlot extends Component { +//import PlotLegend from './widget-plot/plot-legend'; +class WidgetPlot extends React.Component { render() { const simulator = this.props.widget.simulator; const simulation = this.props.simulation; @@ -38,15 +37,15 @@ class WidgetPlot extends Component { const model = simulation.models.find( model => model.simulator.node === simulator.node && model.simulator.simulator === simulator.simulator ); const chosenSignals = this.props.widget.signals; - simulatorData = this.props.data[simulator.node][simulator.simulator]; + simulatorData = this.props.data[simulator.node][simulator.simulator].values[0]; // Query the signals that will be displayed in the legend legendSignals = model.mapping.reduce( (accum, model_signal, signal_index) => { - if (chosenSignals.includes(signal_index)) { - accum.push({ index: signal_index, name: model_signal.name }); - } - return accum; - }, []); + if (chosenSignals.includes(signal_index)) { + accum.push({ index: signal_index, name: model_signal.name }); + } + return accum; + }, []); } return ( @@ -54,9 +53,12 @@ class WidgetPlot extends Component {

{this.props.widget.name}

- +
- ); } diff --git a/src/components/widget-plot/plot.js b/src/components/widget-plot/plot.js index 7b0c3aa..ee29df4 100644 --- a/src/components/widget-plot/plot.js +++ b/src/components/widget-plot/plot.js @@ -7,135 +7,45 @@ * Unauthorized copying of this file, via any medium is strictly prohibited. **********************************************************************************/ -import React, { Component } from 'react'; -import { LineChart } from 'rd3'; -import { scaleOrdinal, schemeCategory10 } from 'd3-scale'; - -class Plot extends Component { - constructor(props) { - super(props); - - this.chartWrapper = null; - - // Initialize plot size and data - this.state = Object.assign( - { size: { w: 0, h: 0 } }, - this.getPlotInitData(true) - ); - } - - // Get an object with 'invisible' init data for the last minute. - // Include start/end timestamps if required. - getPlotInitData(withRangeTimestamps = false) { - - const initSecondTime = Date.now(); - const initFirstTime = initSecondTime - 1000 * 60; // Decrease 1 min - const values = [{ values: [{x: initFirstTime, y: 0}], strokeWidth: 0 }]; - - let output = withRangeTimestamps? - { sequence: 0, values: values, firstTimestamp: initFirstTime, latestTimestamp: initSecondTime, } : - { sequence: 0, values: values }; - - return output; - } - - componentWillReceiveProps(nextProps) { - let nextData = nextProps.simulatorData; - - // handle plot size - const w = this.chartWrapper.offsetWidth - 20; - const h = this.chartWrapper.offsetHeight - 20; - const currentSize = this.state.size; - if (w !== currentSize.w || h !== currentSize.h) { - this.setState({size: { w, h } }); - } - - // If signals were cleared, clear the plot (triggers a new state) - if (this.signalsWereJustCleared(nextProps)) { this.clearPlot(); return; } - - // If no signals have been selected, just leave - if (nextProps.signals == null || nextProps.signals.length === 0) { return; } - - // Identify simulation reset - if (nextData == null || nextData.length === 0 || nextData.values[0].length === 0) { this.clearPlot(); return; } - - // check if new data, otherwise skip - if (this.state.sequence >= nextData.sequence) { return; } - - this.updatePlotData(nextProps); - - } - - signalsWereJustCleared(nextProps) { - - return this.props.signals && - nextProps.signals && - this.props.signals.length > 0 && - nextProps.signals.length === 0; - } - - clearPlot() { - this.setState( this.getPlotInitData(false) ); - } - - updatePlotData(nextProps) { - let nextData = nextProps.simulatorData; - - // get timestamps - var latestTimestamp = nextData.values[0][nextData.values[0].length - 1].x; - var firstTimestamp = latestTimestamp - nextProps.time * 1000; - var firstIndex; - - if (nextData.values[0][0].x < firstTimestamp) { - // find element index representing firstTimestamp - firstIndex = nextData.values[0].findIndex((value) => { - return value.x >= firstTimestamp; - }); - } else { - firstIndex = 0; - firstTimestamp = nextData.values[0][0].x; - latestTimestamp = firstTimestamp + nextProps.time * 1000; - } - - // copy all values for each signal in time region - var values = []; - nextProps.signals.forEach((signal_index, i, arr) => ( - // Include signal index, useful to relate them to the signal selection - values.push( - { - index: signal_index, - values: nextData.values[signal_index].slice(firstIndex, nextData.values[signal_index].length - 1)}) - )); - - this.setState({ values: values, firstTimestamp: firstTimestamp, latestTimestamp: latestTimestamp, sequence: nextData.sequence }); - } +import React from 'react'; +import { scaleLinear, scaleTime } from 'd3-scale'; +import { extent, max } from 'd3-array'; +import { line } from 'd3-shape'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { select } from 'd3-selection'; +class Plot extends React.Component { render() { - // Make tick count proportional to the plot width using a rough scale ratio - var tickCount = Math.round(this.state.size.w / 80); + const leftMargin = 30; + const bottomMargin = 20; + const values = 100; - return ( -
this.chartWrapper = domNode }> - {this.state.sequence != null && - { if (d != null) { return new Date(d.x); } }} - xAxisTickCount={ tickCount } - yAxisLabel={ this.props.yAxisLabel } - hoverAnimation={false} - circleRadius={0} - domain={{ x: [this.state.firstTimestamp, this.state.latestTimestamp] }} - /> - } -
+ let data = this.props.data; + + if (data.length > values) { + data = data.slice(data.length - values); + } + + const xScale = scaleTime().domain(extent(data, p => new Date(p.x))).range([leftMargin, this.props.width]); + const yScale = scaleLinear().domain(extent(data, p => p.y)).range([this.props.height, bottomMargin]); + + const xAxis = axisBottom().scale(xScale).ticks(5); + const yAxis = axisLeft().scale(yScale).ticks(5); + + const sparkLine = line().x(p => xScale(p.x)).y(p => yScale(p.y)); + const linePath = sparkLine(data); + + return( + + select(node).call(xAxis)} style={{ transform: `translateY(${this.props.height}px)` }} /> + select(node).call(yAxis)} style={{ transform: `translateX(${leftMargin}px)`}} /> + + + + + ); } - } export default Plot;