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:
parent
50d8f52508
commit
37af4f5151
3 changed files with 53 additions and 137 deletions
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue