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

extracted plot and legend to separate components, reused them in plot widget

This commit is contained in:
Ricardo Hernandez-Montoya 2017-04-10 14:43:25 +02:00
parent a6cbecd811
commit 58f49c13a1
5 changed files with 219 additions and 175 deletions

View file

@ -11,21 +11,16 @@ import React, { Component } from 'react';
import { LineChart } from 'rd3';
import { scaleOrdinal, schemeCategory10 } from 'd3-scale';
import classNames from 'classnames';
import { FormGroup, Checkbox } from 'react-bootstrap';
import Plot from './widget-plot/plot';
import PlotLegend from './widget-plot/plot-legend';
class WidgetPlotTable extends Component {
constructor(props) {
super(props);
this.chartWrapper = null;
this.state = {
size: { w: 0, h: 0 },
firstTimestamp: 0,
latestTimestamp: 0,
sequence: null,
values: [],
preselectedSignals: [],
signals: []
};
@ -35,15 +30,6 @@ class WidgetPlotTable extends Component {
// check data
const simulator = nextProps.widget.simulator;
// 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 } });
}
// this.setState({ size: { w: this.props.widget.width - 100, h: this.props.widget.height - 77 }});
// Update internal selected signals state with props (different array objects)
if (this.props.widget.signals !== nextProps.widget.signals) {
this.setState( {signals: nextProps.widget.signals});
@ -55,28 +41,12 @@ class WidgetPlotTable extends Component {
// Update the currently selected signals by intersecting with the preselected signals
// Do the same with the plot values
var intersection = this.computeIntersection(nextProps.widget.preselectedSignals, nextProps.widget.signals);
this.setState({
signals: intersection,
values: this.state.values.filter( (values) => intersection.includes(values.index))
});
this.setState({ signals: intersection });
this.updatePreselectedSignalsState(nextProps);
return;
}
// Identify simulation reset
if (nextProps.simulation == null || nextProps.data == null || nextProps.data[simulator] == null || nextProps.data[simulator].length === 0 || nextProps.data[simulator].values[0].length === 0) {
// clear values
this.setState({ values: [], sequence: null });
return;
}
// check if new data, otherwise skip
if (this.state.sequence >= nextProps.data[simulator].sequence) {
return;
}
this.updatePlotData(nextProps);
}
// Perform the intersection of the lists, alternatively could be done with Sets ensuring unique values
@ -110,38 +80,6 @@ class WidgetPlotTable extends Component {
this.setState({ preselectedSignals: preselectedSignals });
}
updatePlotData(nextProps) {
const simulator = nextProps.widget.simulator;
// get timestamps
var latestTimestamp = nextProps.data[simulator].values[0][nextProps.data[simulator].values[0].length - 1].x;
var firstTimestamp = latestTimestamp - nextProps.widget.time * 1000;
var firstIndex;
if (nextProps.data[simulator].values[0][0].x < firstTimestamp) {
// find element index representing firstTimestamp
firstIndex = nextProps.data[simulator].values[0].findIndex((value) => {
return value.x >= firstTimestamp;
});
} else {
firstIndex = 0;
firstTimestamp = nextProps.data[simulator].values[0][0].x;
latestTimestamp = firstTimestamp + nextProps.widget.time * 1000;
}
// copy all values for each signal in time region
var values = [];
this.state.signals.forEach((signal_index, i, arr) => (
// Include signal index, useful to relate them to the signal selection
values.push(
{
index: signal_index,
values: nextProps.data[simulator].values[signal_index].slice(firstIndex, nextProps.data[simulator].values[signal_index].length - 1)})
));
this.setState({ values: values, firstTimestamp: firstTimestamp, latestTimestamp: latestTimestamp, sequence: nextProps.data[simulator].sequence });
}
updateSignalSelection(signal_index, checked) {
// Update the selected signals and propagate to parent component
var new_widget = Object.assign({}, this.props.widget, {
@ -153,6 +91,10 @@ class WidgetPlotTable extends Component {
render() {
var checkBoxes = [];
// Data passed to plot
let simulator = this.props.widget.simulator;
let simulatorData = this.props.data[simulator];
if (this.state.preselectedSignals && this.state.preselectedSignals.length > 0) {
// Create checkboxes using the signal indices from simulation model
checkBoxes = this.state.preselectedSignals.map( (signal) => {
@ -166,9 +108,16 @@ class WidgetPlotTable extends Component {
});
}
// Make tick count proportional to the plot width using a rough scale ratio
var tickCount = Math.round(this.state.size.w / 80);
var colorScale = scaleOrdinal(schemeCategory10);
// Prepare an array with the signals to show in the legend
var legendSignals = this.state.preselectedSignals.reduce( (accum, signal, i) => {
if (this.state.signals.includes(signal.index)) {
accum.push({
index: signal.index,
name: signal.name
})
}
return accum;
}, []);
return (
<div className="plot-table-widget" ref="wrapper">
@ -186,34 +135,10 @@ class WidgetPlotTable extends Component {
</div>
<div className="widget-plot">
<div className="chart-wrapper" ref={ (domNode) => this.chartWrapper = domNode }>
{this.state.sequence &&
<LineChart
width={ this.state.size.w || 100 }
height={ this.state.size.h || 100 }
data={this.state.values }
colors={ scaleOrdinal(schemeCategory10) }
gridHorizontal={true}
xAccessor={(d) => { if (d != null) { return new Date(d.x); } }}
xAxisTickCount={ tickCount }
hoverAnimation={false}
circleRadius={0}
domain={{ x: [this.state.firstTimestamp, this.state.latestTimestamp] }}
/>
}
</div>
<Plot signals={ this.state.signals } time={ this.props.widget.time } simulatorData={ simulatorData } />
</div>
</div>
<div className="plot-legend">
{
this.state.preselectedSignals.reduce( (accum, signal, i) => {
if (this.state.signals.includes(signal.index)) {
accum.push(<div key={signal.index} className="signal-legend"><span className="legend-color" style={{ background: colorScale(signal.index) }}>&nbsp;&nbsp;</span> {signal.name} </div>)
}
return accum;
}, [])
}
</div>
<PlotLegend signals={legendSignals} />
</div>
</div>
);

View file

@ -8,81 +8,38 @@
**********************************************************************************/
import React, { Component } from 'react';
import { LineChart } from 'rd3';
import Plot from './widget-plot/plot';
import PlotLegend from './widget-plot/plot-legend';
class WidgetPlot extends Component {
constructor(props) {
super(props);
this.state = {
values: [],
firstTimestamp: 0,
latestTimestamp: 0,
sequence: null
};
}
componentWillReceiveProps(nextProps) {
// check data
const simulator = nextProps.widget.simulator;
if (nextProps.simulation == null || nextProps.data == null || nextProps.data[simulator] == null || nextProps.data[simulator].length === 0 || nextProps.data[simulator].values[0].length === 0) {
// clear values
this.setState({ values: [], sequence: null });
return;
}
// check if new data, otherwise skip
if (this.state.sequence >= nextProps.data[simulator].sequence) {
return;
}
// get timestamps
var latestTimestamp = nextProps.data[simulator].values[0][nextProps.data[simulator].values[0].length - 1].x;
var firstTimestamp = latestTimestamp - nextProps.widget.time * 1000;
var firstIndex;
if (nextProps.data[simulator].values[0][0].x < firstTimestamp) {
// find element index representing firstTimestamp
firstIndex = nextProps.data[simulator].values[0].findIndex((value) => {
return value.x >= firstTimestamp;
});
} else {
firstIndex = 0;
firstTimestamp = nextProps.data[simulator].values[0][0].x;
latestTimestamp = firstTimestamp + nextProps.widget.time * 1000;
}
// copy all values for each signal in time region
var values = [];
nextProps.widget.signals.forEach((signal) => {
values.push({
values: nextProps.data[simulator].values[signal].slice(firstIndex, nextProps.data[simulator].values[signal].length - 1)
});
});
this.setState({ values: values, firstTimestamp: firstTimestamp, latestTimestamp: latestTimestamp, sequence: nextProps.data[simulator].sequence });
}
render() {
if (this.state.sequence == null) {
if (this.props.simulation == null) {
return (<div>Empty</div>);
}
let simulator = this.props.widget.simulator;
let simulation = this.props.simulation;
let model = simulation.models.find( (model) => model.simulator === simulator );
let chosenSignals = this.props.widget.signals;
let simulatorData = this.props.data[simulator];
// Query the signals that will be displayed in the legend
let 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 (
<div style={{ width: '100%', height: '100%' }} ref="wrapper">
<LineChart
width={this.props.widget.width}
height={this.props.widget.height - 20}
data={this.state.values}
title={this.props.widget.name}
gridHorizontal={true}
xAccessor={(d) => { if (d != null) { return new Date(d.x); } }}
hoverAnimation={false}
circleRadius={0}
domain={{ x: [this.state.firstTimestamp, this.state.latestTimestamp] }}
/>
<div className="plot-widget" ref="wrapper">
<div className="widget-plot">
<Plot signals={ this.props.widget.signals } time={ this.props.widget.time } simulatorData={ simulatorData } />
</div>
<PlotLegend signals={legendSignals} />
</div>
);
}

View file

@ -0,0 +1,31 @@
/**
* File: plot-legend.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 10.04.2017
* Copyright: 2017, Institute for Automation of Complex Power Systems, EONERC
* This file is part of VILLASweb. All Rights Reserved. Proprietary and confidential.
* Unauthorized copying of this file, via any medium is strictly prohibited.
**********************************************************************************/
import React, { Component } from 'react';
import { scaleOrdinal, schemeCategory10 } from 'd3-scale';
class PlotLegend extends Component {
// constructor(props) {
// super(props);
// }
render() {
var colorScale = scaleOrdinal(schemeCategory10);
return (
<div className="plot-legend">
{ this.props.signals.map( (signal) =>
<div key={signal.index} className="signal-legend"><span className="legend-color" style={{ background: colorScale(signal.index) }}>&nbsp;&nbsp;</span> {signal.name} </div>)
}
</div>
);
}
}
export default PlotLegend;

View file

@ -0,0 +1,115 @@
/**
* File: plot.js
* Author: Ricardo Hernandez-Montoya <rhernandez@gridhound.de>
* Date: 10.04.2017
* Copyright: 2017, Institute for Automation of Complex Power Systems, EONERC
* This file is part of VILLASweb. All Rights Reserved. Proprietary and confidential.
* 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;
this.state = {
size: { w: 0, h: 0 },
firstTimestamp: 0,
latestTimestamp: 0,
sequence: null,
values: []
};
}
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 } });
}
// Identify simulation reset
if (nextData == null || nextData.length === 0 || nextData.values[0].length === 0) {
// clear values
this.setState({ values: [], sequence: null });
return;
}
// check if new data, otherwise skip
if (this.state.sequence >= nextData.sequence) {
return;
}
this.updatePlotData(nextProps);
}
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 });
}
render() {
// Make tick count proportional to the plot width using a rough scale ratio
var tickCount = Math.round(this.state.size.w / 80);
return (
<div className="chart-wrapper" ref={ (domNode) => this.chartWrapper = domNode }>
{this.state.sequence &&
<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 }
hoverAnimation={false}
circleRadius={0}
domain={{ x: [this.state.firstTimestamp, this.state.latestTimestamp] }}
/>
}
</div>
);
}
}
export default Plot;

View file

@ -186,8 +186,33 @@ div[class*="-widget"] .btn[disabled], .btn.disabled, div[class*="-widget"] input
.plot-table-widget input[type="checkbox"] {
display: none;
}
.plot-table-widget .widget-plot {
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
}
/* End PlotTable Widget */
.plot-table-widget .plot-legend {
/* Plot Widget */
.plot-widget {
display: -webkit-flex;
display: flex;
flex-direction: column;
}
.plot-widget .widget-plot {
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
}
/* End Plot Widget */
/* Plots */
.chart-wrapper {
height: 100%;
width: 100%;
}
.plot-legend {
display: -webkit-flex;
display: flex;
flex-wrap: wrap;
@ -195,28 +220,19 @@ div[class*="-widget"] .btn[disabled], .btn.disabled, div[class*="-widget"] input
margin-bottom: 5px;
}
.plot-table-widget .signal-legend {
.signal-legend {
font-size: 0.8em;
font-weight: 700;
overflow-x: hidden;
}
.plot-table-widget .legend-color {
.legend-color {
height: 50%;
display: inline-block;
vertical-align: middle;
}
.plot-table-widget .widget-plot {
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
}
.plot-table-widget .chart-wrapper {
height: 100%;
width: 100%;
}
/* End PlotTable Widget */
/* End Plots */
/*.single-value-widget {
position: absolute;