From 37af4f5151558ffb8261e5e14f0473faca6dbd26 Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Wed, 26 Jul 2017 23:10:29 +0200 Subject: [PATCH 1/6] 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; From 15fd134f9e5ba05d320eec6a8d45e6401ebbf54c Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Thu, 27 Jul 2017 00:28:19 +0200 Subject: [PATCH 2/6] Add time scaling --- src/components/widget-plot.js | 19 ++++++++++--------- src/components/widget-plot/plot.js | 29 +++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/components/widget-plot.js b/src/components/widget-plot.js index 56b5b2a..2fb5b93 100644 --- a/src/components/widget-plot.js +++ b/src/components/widget-plot.js @@ -28,24 +28,24 @@ class WidgetPlot extends React.Component { render() { const simulator = this.props.widget.simulator; const simulation = this.props.simulation; - let legendSignals = []; + //let legendSignals = []; let simulatorData = []; // Proceed if a simulation with models and a simulator are available if (simulator && simulation && simulation.models.length > 0) { - const model = simulation.models.find( model => model.simulator.node === simulator.node && model.simulator.simulator === simulator.simulator ); - const chosenSignals = this.props.widget.signals; + //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].values[0]; // Query the signals that will be displayed in the legend - legendSignals = model.mapping.reduce( (accum, model_signal, signal_index) => { + /*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; - }, []); + }, []);*/ } return ( @@ -53,10 +53,11 @@ class WidgetPlot extends React.Component {

{this.props.widget.name}

-
diff --git a/src/components/widget-plot/plot.js b/src/components/widget-plot/plot.js index ee29df4..ce30589 100644 --- a/src/components/widget-plot/plot.js +++ b/src/components/widget-plot/plot.js @@ -9,24 +9,37 @@ import React from 'react'; import { scaleLinear, scaleTime } from 'd3-scale'; -import { extent, max } from 'd3-array'; +import { extent } 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() { - const leftMargin = 30; - const bottomMargin = 20; - const values = 100; - + // check if data is valid + if (this.props.data == null || this.props.data.length === 0) return false; + + // only show data in requested time let data = this.props.data; - if (data.length > values) { - data = data.slice(data.length - values); + const firstTimestamp = data[data.length - 1].x - this.props.time * 1000; + if (data[0].x < firstTimestamp) { + const index = data.findIndex(value => value.x >= firstTimestamp - 100); + if (index > 0) { + data = data.slice(index - 1); + } } - const xScale = scaleTime().domain(extent(data, p => new Date(p.x))).range([leftMargin, this.props.width]); + // calculate paths for data + const leftMargin = 30; + const bottomMargin = 20; + + let xRange = extent(data, p => new Date(p.x)); + if (xRange[1] - xRange[0] < this.props.time * 1000) { + xRange[0] = xRange[1] - this.props.time * 1000; + } + + const xScale = scaleTime().domain(xRange).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); From 9119f6380db3711fff2f0afc814753dc89809c5c Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Thu, 27 Jul 2017 01:11:20 +0200 Subject: [PATCH 3/6] Add multiple lines to plot --- src/components/widget-plot.js | 4 +- src/components/widget-plot/plot.js | 78 ++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/components/widget-plot.js b/src/components/widget-plot.js index 2fb5b93..0c3d641 100644 --- a/src/components/widget-plot.js +++ b/src/components/widget-plot.js @@ -37,7 +37,9 @@ class WidgetPlot extends React.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].values[0]; + simulatorData = this.props.data[simulator.node][simulator.simulator].values.filter((values, index) => ( + this.props.widget.signals.findIndex(value => value === index) !== -1 + )); // Query the signals that will be displayed in the legend /*legendSignals = model.mapping.reduce( (accum, model_signal, signal_index) => { diff --git a/src/components/widget-plot/plot.js b/src/components/widget-plot/plot.js index ce30589..7749f48 100644 --- a/src/components/widget-plot/plot.js +++ b/src/components/widget-plot/plot.js @@ -14,47 +14,75 @@ import { line } from 'd3-shape'; import { axisBottom, axisLeft } from 'd3-axis'; import { select } from 'd3-selection'; -class Plot extends React.Component { - render() { - // check if data is valid - if (this.props.data == null || this.props.data.length === 0) return false; - - // only show data in requested time - let data = this.props.data; +const leftMargin = 30; +const bottomMargin = 20; - const firstTimestamp = data[data.length - 1].x - this.props.time * 1000; - if (data[0].x < firstTimestamp) { - const index = data.findIndex(value => value.x >= firstTimestamp - 100); - if (index > 0) { - data = data.slice(index - 1); - } +class Plot extends React.Component { + constructor(props) { + super(props); + + this.state = { + data: null + }; + } + + componentWillReceiveProps(nextProps) { + // check if data is valid + if (nextProps.data == null || nextProps.data.length === 0 || nextProps.data[0].length === 0) { + this.setState({ data: null }); + return; + } + + // only show data in requested time + let data = nextProps.data; + + const firstTimestamp = data[0][data[0].length - 1].x - this.props.time * 1000; + if (data[0][0].x < firstTimestamp) { + // only show data in range (+100 ms) + const index = data[0].findIndex(value => value.x >= firstTimestamp - 100); + data = data.map(values => values.slice(index)); } // calculate paths for data - const leftMargin = 30; - const bottomMargin = 20; - - let xRange = extent(data, p => new Date(p.x)); - if (xRange[1] - xRange[0] < this.props.time * 1000) { - xRange[0] = xRange[1] - this.props.time * 1000; + let xRange = extent(data[0], p => new Date(p.x)); + if (xRange[1] - xRange[0] < nextProps.time * 1000) { + xRange[0] = xRange[1] - nextProps.time * 1000; } + + let yRange = [0, 0]; + + data.map(values => { + const range = extent(values, p => p.y); + if (range[0] < yRange[0]) yRange[0] = range[0]; + if (range[1] > yRange[1]) yRange[1] = range[1]; + + return values; + }); - const xScale = scaleTime().domain(xRange).range([leftMargin, this.props.width]); - const yScale = scaleLinear().domain(extent(data, p => p.y)).range([this.props.height, bottomMargin]); + const xScale = scaleTime().domain(xRange).range([leftMargin, nextProps.width]); + const yScale = scaleLinear().domain(yRange).range([nextProps.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); + + // generate paths from data + const lines = data.map((values, index) => ); + + this.setState({ data: lines, xAxis, yAxis }); + } + + render() { + if (this.state.data == null) return false; return( - select(node).call(xAxis)} style={{ transform: `translateY(${this.props.height}px)` }} /> - select(node).call(yAxis)} style={{ transform: `translateX(${leftMargin}px)`}} /> + select(node).call(this.state.xAxis)} style={{ transform: `translateY(${this.props.height}px)` }} /> + select(node).call(this.state.yAxis)} style={{ transform: `translateX(${leftMargin}px)`}} /> - + {this.state.data} ); From 3265838c39524dd925e90abfd1e39b8da42b40e7 Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Thu, 27 Jul 2017 01:54:34 +0200 Subject: [PATCH 4/6] Add signal legend. Add time format. --- package.json | 1 + src/components/widget-plot.js | 16 +++++++++------- src/components/widget-plot/plot.js | 17 ++++++++++------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index e71b043..308d8c3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "d3-scale": "^1.0.6", "d3-selection": "^1.1.0", "d3-shape": "^1.2.0", + "d3-time-format": "^2.0.5", "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 0c3d641..fb89a07 100644 --- a/src/components/widget-plot.js +++ b/src/components/widget-plot.js @@ -22,32 +22,32 @@ import React from 'react'; import Plot from './widget-plot/plot'; -//import PlotLegend from './widget-plot/plot-legend'; +import PlotLegend from './widget-plot/plot-legend'; class WidgetPlot extends React.Component { render() { const simulator = this.props.widget.simulator; const simulation = this.props.simulation; - //let legendSignals = []; + let legendSignals = []; let simulatorData = []; // Proceed if a simulation with models and a simulator are available if (simulator && simulation && simulation.models.length > 0) { - //const model = simulation.models.find( model => model.simulator.node === simulator.node && model.simulator.simulator === simulator.simulator ); - //const chosenSignals = this.props.widget.signals; + 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].values.filter((values, index) => ( this.props.widget.signals.findIndex(value => value === index) !== -1 )); // Query the signals that will be displayed in the legend - /*legendSignals = model.mapping.reduce( (accum, model_signal, signal_index) => { + 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; - }, []);*/ + }, []); } return ( @@ -57,11 +57,13 @@ class WidgetPlot extends React.Component {
+ + ); } diff --git a/src/components/widget-plot/plot.js b/src/components/widget-plot/plot.js index 7749f48..8af08f4 100644 --- a/src/components/widget-plot/plot.js +++ b/src/components/widget-plot/plot.js @@ -8,11 +8,12 @@ **********************************************************************************/ import React from 'react'; -import { scaleLinear, scaleTime } from 'd3-scale'; +import { scaleLinear, scaleTime, scaleOrdinal, schemeCategory10 } from 'd3-scale'; import { extent } from 'd3-array'; import { line } from 'd3-shape'; import { axisBottom, axisLeft } from 'd3-axis'; import { select } from 'd3-selection'; +import { timeFormat } from 'd3-time-format'; const leftMargin = 30; const bottomMargin = 20; @@ -59,16 +60,18 @@ class Plot extends React.Component { return values; }); + // create scale functions for both axes const xScale = scaleTime().domain(xRange).range([leftMargin, nextProps.width]); const yScale = scaleLinear().domain(yRange).range([nextProps.height, bottomMargin]); - - const xAxis = axisBottom().scale(xScale).ticks(5); + + const xAxis = axisBottom().scale(xScale).ticks(5).tickFormat(date => timeFormat("%M:%S")(date)); const yAxis = axisLeft().scale(yScale).ticks(5); - const sparkLine = line().x(p => xScale(p.x)).y(p => yScale(p.y)); - // generate paths from data - const lines = data.map((values, index) => ); + const sparkLine = line().x(p => xScale(p.x)).y(p => yScale(p.y)); + const lineColor = scaleOrdinal(schemeCategory10); + + const lines = data.map((values, index) => ); this.setState({ data: lines, xAxis, yAxis }); } @@ -81,7 +84,7 @@ class Plot extends React.Component { select(node).call(this.state.xAxis)} style={{ transform: `translateY(${this.props.height}px)` }} /> select(node).call(this.state.yAxis)} style={{ transform: `translateX(${leftMargin}px)`}} /> - + {this.state.data} From 4c73eb44f99937e477aed6738729c76a379e3ec0 Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Thu, 27 Jul 2017 02:00:50 +0200 Subject: [PATCH 5/6] Update plot-table with new plot --- src/components/widget-plot-table.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/widget-plot-table.js b/src/components/widget-plot-table.js index 7862a05..c674773 100644 --- a/src/components/widget-plot-table.js +++ b/src/components/widget-plot-table.js @@ -108,7 +108,9 @@ class WidgetPlotTable extends Component { let simulatorData = []; if (this.props.data[simulator.node] != null && this.props.data[simulator.node][simulator.simulator] != null) { - simulatorData = this.props.data[simulator.node][simulator.simulator]; + simulatorData = this.props.data[simulator.node][simulator.simulator].values.filter((values, index) => ( + this.props.widget.signals.findIndex(value => value === index) !== -1 + )); } if (this.state.preselectedSignals && this.state.preselectedSignals.length > 0) { @@ -153,7 +155,12 @@ class WidgetPlotTable extends Component {
- +
From efcb49d05a9723bb236e22648a2eec78e8fc2b84 Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Thu, 27 Jul 2017 08:47:20 +0200 Subject: [PATCH 6/6] Remove unneeded dependencies --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 308d8c3..57c7657 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "gaugeJS": "^1.3.2", "immutable": "^3.8.1", "rc-slider": "^7.0.1", - "rd3": "^0.7.4", "react": "^15.4.2", "react-bootstrap": "^0.31.1", "react-contextmenu": "^2.3.0",