diff --git a/package.json b/package.json index 98e3fa3..57c7657 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,17 @@ "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", + "d3-time-format": "^2.0.5", "es6-promise": "^4.0.5", "flux": "^3.1.2", "gaugeJS": "^1.3.2", "immutable": "^3.8.1", "rc-slider": "^7.0.1", - "rd3": "^0.7.4", "react": "^15.4.2", "react-bootstrap": "^0.31.1", "react-contextmenu": "^2.3.0", diff --git a/src/components/widget-plot-table.js b/src/components/widget-plot-table.js index 7862a05..c674773 100644 --- a/src/components/widget-plot-table.js +++ b/src/components/widget-plot-table.js @@ -108,7 +108,9 @@ class WidgetPlotTable extends Component { let simulatorData = []; if (this.props.data[simulator.node] != null && this.props.data[simulator.node][simulator.simulator] != null) { - simulatorData = this.props.data[simulator.node][simulator.simulator]; + simulatorData = this.props.data[simulator.node][simulator.simulator].values.filter((values, index) => ( + this.props.widget.signals.findIndex(value => value === index) !== -1 + )); } if (this.state.preselectedSignals && this.state.preselectedSignals.length > 0) { @@ -153,7 +155,12 @@ class WidgetPlotTable extends Component {
- +
diff --git a/src/components/widget-plot.js b/src/components/widget-plot.js index c02aa97..fb89a07 100644 --- a/src/components/widget-plot.js +++ b/src/components/widget-plot.js @@ -19,13 +19,12 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -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 { - +class WidgetPlot extends React.Component { render() { const simulator = this.props.widget.simulator; const simulation = this.props.simulation; @@ -38,15 +37,17 @@ 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.filter((values, index) => ( + this.props.widget.signals.findIndex(value => value === index) !== -1 + )); // 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,8 +55,14 @@ class WidgetPlot extends Component {

{this.props.widget.name}

- +
+ ); diff --git a/src/components/widget-plot/plot.js b/src/components/widget-plot/plot.js index 7b0c3aa..8af08f4 100644 --- a/src/components/widget-plot/plot.js +++ b/src/components/widget-plot/plot.js @@ -7,135 +7,89 @@ * 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'; +import React from 'react'; +import { scaleLinear, scaleTime, scaleOrdinal, schemeCategory10 } from 'd3-scale'; +import { extent } from 'd3-array'; +import { line } from 'd3-shape'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { select } from 'd3-selection'; +import { timeFormat } from 'd3-time-format'; -class Plot extends Component { +const leftMargin = 30; +const bottomMargin = 20; + +class Plot extends React.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; + this.state = { + data: null + }; } 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 } }); + // check if data is valid + if (nextProps.data == null || nextProps.data.length === 0 || nextProps.data[0].length === 0) { + this.setState({ data: null }); + return; } - // 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; + // only show data in requested time + let data = nextProps.data; + + const firstTimestamp = data[0][data[0].length - 1].x - this.props.time * 1000; + if (data[0][0].x < firstTimestamp) { + // only show data in range (+100 ms) + const index = data[0].findIndex(value => value.x >= firstTimestamp - 100); + data = data.map(values => values.slice(index)); } - // 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)}) - )); + // calculate paths for data + let xRange = extent(data[0], p => new Date(p.x)); + if (xRange[1] - xRange[0] < nextProps.time * 1000) { + xRange[0] = xRange[1] - nextProps.time * 1000; + } - this.setState({ values: values, firstTimestamp: firstTimestamp, latestTimestamp: latestTimestamp, sequence: nextData.sequence }); + let yRange = [0, 0]; + + data.map(values => { + const range = extent(values, p => p.y); + if (range[0] < yRange[0]) yRange[0] = range[0]; + if (range[1] > yRange[1]) yRange[1] = range[1]; + + return values; + }); + + // create scale functions for both axes + const xScale = scaleTime().domain(xRange).range([leftMargin, nextProps.width]); + const yScale = scaleLinear().domain(yRange).range([nextProps.height, bottomMargin]); + + const xAxis = axisBottom().scale(xScale).ticks(5).tickFormat(date => timeFormat("%M:%S")(date)); + const yAxis = axisLeft().scale(yScale).ticks(5); + + // generate paths from data + const sparkLine = line().x(p => xScale(p.x)).y(p => yScale(p.y)); + const lineColor = scaleOrdinal(schemeCategory10); + + const lines = data.map((values, index) => ); + + this.setState({ data: lines, xAxis, yAxis }); } render() { - // Make tick count proportional to the plot width using a rough scale ratio - var tickCount = Math.round(this.state.size.w / 80); + if (this.state.data == null) return false; - return ( -
this.chartWrapper = domNode }> - {this.state.sequence != null && - { 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] }} - /> - } -
+ return( + + select(node).call(this.state.xAxis)} style={{ transform: `translateY(${this.props.height}px)` }} /> + select(node).call(this.state.yAxis)} style={{ transform: `translateX(${leftMargin}px)`}} /> + + + {this.state.data} + + ); } - } export default Plot;