1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/web/ synced 2025-03-09 00:00:01 +01:00

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.
This commit is contained in:
Markus Grigull 2017-07-26 23:10:29 +02:00
parent 50d8f52508
commit 37af4f5151
3 changed files with 53 additions and 137 deletions

View file

@ -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",

View file

@ -19,13 +19,12 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
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 {
<h4>{this.props.widget.name}</h4>
<div className="widget-plot">
<Plot signals={ this.props.widget.signals } time={ this.props.widget.time } simulatorData={ simulatorData } yAxisLabel={ this.props.widget.ylabel } />
<Plot
data={simulatorData}
height={this.props.widget.height - 80}
width={this.props.widget.width - 20}
/>
</div>
<PlotLegend signals={legendSignals} />
</div>
);
}

View file

@ -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 (
<div className="chart-wrapper" ref={ (domNode) => this.chartWrapper = domNode }>
{this.state.sequence != null &&
<LineChart
width={ this.state.size.w || 100 }
height={ this.state.size.h || 100 }
margins={{top: 10, right: 0, bottom: 20, left: 45 }}
data={this.state.values }
colors={ scaleOrdinal(schemeCategory10) }
gridHorizontal={true}
xAccessor={(d) => { 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] }}
/>
}
</div>
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(
<svg width={this.props.width + leftMargin} height={this.props.height + bottomMargin}>
<g ref={node => select(node).call(xAxis)} style={{ transform: `translateY(${this.props.height}px)` }} />
<g ref={node => select(node).call(yAxis)} style={{ transform: `translateX(${leftMargin}px)`}} />
<g style={{ fill: 'none', stroke: 'blue' }}>
<path d={linePath} />
</g>
</svg>
);
}
}
export default Plot;