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:
parent
a6cbecd811
commit
58f49c13a1
5 changed files with 219 additions and 175 deletions
|
@ -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) }}> </span> {signal.name} </div>)
|
||||
}
|
||||
return accum;
|
||||
}, [])
|
||||
}
|
||||
</div>
|
||||
<PlotLegend signals={legendSignals} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
31
src/components/widget-plot/plot-legend.js
Normal file
31
src/components/widget-plot/plot-legend.js
Normal 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) }}> </span> {signal.name} </div>)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlotLegend;
|
115
src/components/widget-plot/plot.js
Normal file
115
src/components/widget-plot/plot.js
Normal 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;
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue