From 58f49c13a195cd7e36b4ec62d1e7700fedde820c Mon Sep 17 00:00:00 2001 From: Ricardo Hernandez-Montoya Date: Mon, 10 Apr 2017 14:43:25 +0200 Subject: [PATCH] extracted plot and legend to separate components, reused them in plot widget --- src/components/widget-plot-table.js | 115 ++++------------------ src/components/widget-plot.js | 91 +++++------------ src/components/widget-plot/plot-legend.js | 31 ++++++ src/components/widget-plot/plot.js | 115 ++++++++++++++++++++++ src/styles/widgets.css | 42 +++++--- 5 files changed, 219 insertions(+), 175 deletions(-) create mode 100644 src/components/widget-plot/plot-legend.js create mode 100644 src/components/widget-plot/plot.js diff --git a/src/components/widget-plot-table.js b/src/components/widget-plot-table.js index 3f67bc6..23fe0c0 100644 --- a/src/components/widget-plot-table.js +++ b/src/components/widget-plot-table.js @@ -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 (
@@ -186,34 +135,10 @@ class WidgetPlotTable extends Component {
-
this.chartWrapper = domNode }> - {this.state.sequence && - { if (d != null) { return new Date(d.x); } }} - xAxisTickCount={ tickCount } - hoverAnimation={false} - circleRadius={0} - domain={{ x: [this.state.firstTimestamp, this.state.latestTimestamp] }} - /> - } -
+
-
- { - this.state.preselectedSignals.reduce( (accum, signal, i) => { - if (this.state.signals.includes(signal.index)) { - accum.push(
   {signal.name}
) - } - return accum; - }, []) - } -
+ ); diff --git a/src/components/widget-plot.js b/src/components/widget-plot.js index 96de856..5c1e785 100644 --- a/src/components/widget-plot.js +++ b/src/components/widget-plot.js @@ -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 (
Empty
); } + 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 ( -
- { if (d != null) { return new Date(d.x); } }} - hoverAnimation={false} - circleRadius={0} - domain={{ x: [this.state.firstTimestamp, this.state.latestTimestamp] }} - /> +
+
+ +
+
); } diff --git a/src/components/widget-plot/plot-legend.js b/src/components/widget-plot/plot-legend.js new file mode 100644 index 0000000..3771a5c --- /dev/null +++ b/src/components/widget-plot/plot-legend.js @@ -0,0 +1,31 @@ +/** + * File: plot-legend.js + * Author: Ricardo Hernandez-Montoya + * 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 ( +
+ { this.props.signals.map( (signal) => +
   {signal.name}
) + } +
+ ); + } +} + +export default PlotLegend; \ No newline at end of file diff --git a/src/components/widget-plot/plot.js b/src/components/widget-plot/plot.js new file mode 100644 index 0000000..40c842c --- /dev/null +++ b/src/components/widget-plot/plot.js @@ -0,0 +1,115 @@ +/** + * File: plot.js + * Author: Ricardo Hernandez-Montoya + * 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 ( +
this.chartWrapper = domNode }> + {this.state.sequence && + { if (d != null) { return new Date(d.x); } }} + xAxisTickCount={ tickCount } + hoverAnimation={false} + circleRadius={0} + domain={{ x: [this.state.firstTimestamp, this.state.latestTimestamp] }} + /> + } +
+ ); + } + +} + +export default Plot; \ No newline at end of file diff --git a/src/styles/widgets.css b/src/styles/widgets.css index c0a354a..45956cb 100644 --- a/src/styles/widgets.css +++ b/src/styles/widgets.css @@ -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;