From 7825d119de8d3600c52771d3d82872e11ee965dc Mon Sep 17 00:00:00 2001 From: Markus Grigull Date: Mon, 18 Sep 2017 00:29:55 +0200 Subject: [PATCH] Add gauge widget color zones --- .../dialog/edit-widget-color-zones-control.js | 131 +++++++++++++ .../dialog/edit-widget-control-creator.js | 7 +- .../dialog/edit-widget-min-max-control.js | 64 +++++++ src/components/dialog/edit-widget.js | 4 +- src/components/table-column.js | 4 +- src/components/table.js | 8 +- src/components/widget-factory.js | 9 +- src/components/widget-gauge.js | 178 ++++++++++++------ src/styles/app.css | 5 + 9 files changed, 346 insertions(+), 64 deletions(-) create mode 100644 src/components/dialog/edit-widget-color-zones-control.js create mode 100644 src/components/dialog/edit-widget-min-max-control.js diff --git a/src/components/dialog/edit-widget-color-zones-control.js b/src/components/dialog/edit-widget-color-zones-control.js new file mode 100644 index 0000000..458a4ce --- /dev/null +++ b/src/components/dialog/edit-widget-color-zones-control.js @@ -0,0 +1,131 @@ +/** + * File: edit-widget-color-zones-control.js + * Author: Markus Grigull + * Date: 20.08.2017 + * + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import { FormGroup, ControlLabel, Button, Glyphicon } from 'react-bootstrap'; + +import Table from '../table'; +import TableColumn from '../table-column'; + +class EditWidgetColorZonesControl extends React.Component { + constructor(props) { + super(props); + + this.state = { + widget: { + zones: [] + }, + selectedZones: [] + }; + } + + componentWillReceiveProps(nextProps) { + this.setState({ widget: nextProps.widget }); + } + + addZone = () => { + // add row + const widget = this.state.widget; + widget.zones.push({ strokeStyle: 'ffffff', min: 0, max: 100 }); + + this.setState({ widget }); + + this.sendEvent(widget); + } + + removeZones = () => { + // remove zones + const widget = this.state.widget; + + this.state.selectedZones.forEach(row => { + widget.zones.splice(row, 1); + }); + + this.setState({ selectedZones: [], widget }); + + this.sendEvent(widget); + } + + changeCell = (event, row, column) => { + // change row + const widget = this.state.widget; + + if (column === 1) { + widget.zones[row].strokeStyle = event.target.value; + } else if (column === 2) { + widget.zones[row].min = event.target.value; + } else if (column === 3) { + widget.zones[row].max = event.target.value; + } + + this.setState({ widget }); + + this.sendEvent(widget); + } + + sendEvent(widget) { + // create event + const event = { + target: { + id: 'zones', + value: widget.zones + } + }; + + this.props.handleChange(event); + } + + checkedCell = (row, event) => { + // update selected rows + const selectedZones = this.state.selectedZones; + + if (event.target.checked) { + if (selectedZones.indexOf(row) === -1) { + selectedZones.push(row); + } + } else { + let index = selectedZones.indexOf(row); + if (row > -1) { + selectedZones.splice(index, 1); + } + } + + this.setState({ selectedZones }); + } + + render() { + return + Color zones + + + + + + +
+ + + +
; + } +} + +export default EditWidgetColorZonesControl; diff --git a/src/components/dialog/edit-widget-control-creator.js b/src/components/dialog/edit-widget-control-creator.js index 87fd786..8846079 100644 --- a/src/components/dialog/edit-widget-control-creator.js +++ b/src/components/dialog/edit-widget-control-creator.js @@ -32,6 +32,8 @@ import EditWidgetOrientation from './edit-widget-orientation'; import EditWidgetAspectControl from './edit-widget-aspect-control'; import EditWidgetTextSizeControl from './edit-widget-text-size-control'; import EditWidgetCheckboxControl from './edit-widget-checkbox-control'; +import EditWidgetColorZonesControl from './edit-widget-color-zones-control'; +import EditWidgetMinMaxControl from './edit-widget-min-max-control'; export default function createControls(widgetType = null, widget = null, sessionToken = null, files = null, validateForm, simulation, handleChange) { // Use a list to concatenate the controls according to the widget type @@ -79,7 +81,10 @@ export default function createControls(widgetType = null, widget = null, session dialogControls.push( validateForm(id)} handleChange={e => handleChange(e)} />, validateForm(id)} simulation={simulation} handleChange={(e) => gaugeBoundOnChange(e) } />, - validateForm(id)} simulation={simulation} handleChange={(e) => handleChange(e)} /> + validateForm(id)} simulation={simulation} handleChange={(e) => handleChange(e)} />, + handleChange(e)} />, + handleChange(e)} />, + handleChange(e)} /> ); break; case 'PlotTable': diff --git a/src/components/dialog/edit-widget-min-max-control.js b/src/components/dialog/edit-widget-min-max-control.js new file mode 100644 index 0000000..13d178a --- /dev/null +++ b/src/components/dialog/edit-widget-min-max-control.js @@ -0,0 +1,64 @@ +/** + * File: edit-widget-min-max-control.js + * Author: Markus Grigull + * Date: 30.08.2017 + * + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import { FormGroup, FormControl, ControlLabel, Checkbox, Table } from 'react-bootstrap'; + +class EditWidgetMinMaxControl extends React.Component { + constructor(props) { + super(props); + + const widget = {}; + widget[props.controlID + "UseMinMax"] = false; + widget[props.controlId + "Min"] = 0; + widget[props.controlId + "Max"] = 1; + + this.state = { + widget + }; + } + + componentWillReceiveProps(nextProps) { + this.setState({ widget: nextProps.widget }); + } + + render() { + return + {this.props.label} + this.props.handleChange(e)}>Enable min-max + + + + + + + + +
+ Min: this.props.handleChange(e)} /> + + Max: this.props.handleChange(e)} /> +
+
; + } +} + +export default EditWidgetMinMaxControl; diff --git a/src/components/dialog/edit-widget.js b/src/components/dialog/edit-widget.js index e408753..91802e0 100644 --- a/src/components/dialog/edit-widget.js +++ b/src/components/dialog/edit-widget.js @@ -53,7 +53,7 @@ class EditWidgetDialog extends React.Component { assignAspectRatio(changeObject, fileId) { // get aspect ratio of file - let file = this.props.files.find(element => element._id === fileId); + const file = this.props.files.find(element => element._id === fileId); // scale width to match aspect const aspectRatio = file.dimensions.width / file.dimensions.height; @@ -94,6 +94,8 @@ class EditWidgetDialog extends React.Component { changeObject = this.assignAspectRatio(changeObject, e.target.value); } else if (e.target.type === 'checkbox') { changeObject[e.target.id] = e.target.checked; + } else if (e.target.type === 'number') { + changeObject[e.target.id] = Number(e.target.value); } else { changeObject[e.target.id] = e.target.value; } diff --git a/src/components/table-column.js b/src/components/table-column.js index 2867754..d78ef45 100644 --- a/src/components/table-column.js +++ b/src/components/table-column.js @@ -33,7 +33,9 @@ class TableColumn extends Component { dataIndex: false, inlineEditable: false, clickable: false, - labelKey: null + labelKey: null, + checkbox: false, + checkboxKey: '' }; render() { diff --git a/src/components/table.js b/src/components/table.js index b78a84e..94da5d0 100644 --- a/src/components/table.js +++ b/src/components/table.js @@ -20,7 +20,7 @@ ******************************************************************************/ import React, { Component } from 'react'; -import { Table, Button, Glyphicon, FormControl, Label } from 'react-bootstrap'; +import { Table, Button, Glyphicon, FormControl, Label, Checkbox } from 'react-bootstrap'; import { Link } from 'react-router-dom'; //import TableColumn from './table-column'; @@ -96,6 +96,12 @@ class CustomTable extends Component { cell.push(); } + if (child.props.checkbox) { + const checkboxKey = this.props.checkboxKey; + + cell.push( child.props.onChecked(index, e)}>); + } + return cell; } diff --git a/src/components/widget-factory.js b/src/components/widget-factory.js index 96c678e..010c01f 100644 --- a/src/components/widget-factory.js +++ b/src/components/widget-factory.js @@ -105,10 +105,15 @@ class WidgetFactory { case 'Gauge': widget.simulator = defaultSimulator; widget.signal = 0; - widget.minWidth = 200; + widget.minWidth = 100; widget.minHeight = 150; - widget.width = 200; + widget.width = 150; widget.height = 150; + widget.colorZones = false; + widget.zones = []; + widget.valueMin = 0; + widget.valueMax = 1; + widget.valueUseMinMax = false; break; case 'Box': widget.minWidth = 50; diff --git a/src/components/widget-gauge.js b/src/components/widget-gauge.js index 800f043..984b382 100644 --- a/src/components/widget-gauge.js +++ b/src/components/widget-gauge.js @@ -18,62 +18,31 @@ class WidgetGauge extends Component { this.gauge = null; this.state = { - value: 0 + value: 0, + minValue: 0, + maxValue: 1 }; } - staticLabels(widget_height) { - let label_font_size = Math.floor(widget_height * 0.055); // font scaling factor, integer for performance - return { - font: label_font_size + 'px "Helvetica Neue"', - labels: [0.0, 0.1, 0.5, 0.9, 1.0], - color: "#000000", - fractionDigits: 1 - } - } - - computeGaugeOptions(widget_height) { - return { - angle: -0.25, - lineWidth: 0.2, - pointer: { - length: 0.6, - strokeWidth: 0.035 - }, - radiusScale: 0.9, - colorStart: '#6EA2B0', - colorStop: '#6EA2B0', - strokeColor: '#E0E0E0', - highDpiSupport: true, - staticLabels: this.staticLabels(widget_height) - }; - } - componentDidMount() { - const opts = this.computeGaugeOptions(this.props.widget.height); - this.gauge = new Gauge(this.gaugeCanvas).setOptions(opts); - this.gauge.maxValue = 1; - this.gauge.setMinValue(0); + this.gauge = new Gauge(this.gaugeCanvas).setOptions(this.computeGaugeOptions(this.props.widget)); + this.gauge.maxValue = this.state.maxValue; + this.gauge.setMinValue(this.state.minValue); this.gauge.animationSpeed = 30; this.gauge.set(this.state.value); - } - shouldComponentUpdate(nextProps, nextState) { - - // Check if size changed, resize labels if it did (the canvas itself is scaled with css) - if (this.props.widget.height !== nextProps.widget.height) { - this.updateAfterResize(nextProps.widget.height); - } - - // signal component update only if the value changed - return this.state.value !== nextState.value; + this.updateLabels(this.state.minValue, this.state.maxValue); } componentWillReceiveProps(nextProps) { // update value const simulator = nextProps.widget.simulator; - if (nextProps.data == null || nextProps.data[simulator.node][simulator.simulator] == null || nextProps.data[simulator.node][simulator.simulator].values == null) { + if (nextProps.data == null || nextProps.data[simulator.node] == null + || nextProps.data[simulator.node][simulator.simulator] == null + || nextProps.data[simulator.node][simulator.simulator].length === 0 + || nextProps.data[simulator.node][simulator.simulator].values.length === 0 + || nextProps.data[simulator.node][simulator.simulator].values[0].length === 0) { this.setState({ value: 0 }); return; } @@ -83,36 +52,129 @@ class WidgetGauge extends Component { // Take just 3 decimal positions // Note: Favor this method over Number.toFixed(n) in order to avoid a type conversion, since it returns a String if (signal != null) { - const new_value = Math.round( signal[signal.length - 1].y * 1e3 ) / 1e3; - if (this.state.value !== new_value) { - this.setState({ value: new_value }); + const value = Math.round( signal[signal.length - 1].y * 1e3 ) / 1e3; + if (this.state.value !== value && value != null) { + this.setState({ value }); + + // update min-max if needed + let updateLabels = false; + let minValue = this.state.minValue; + let maxValue = this.state.maxValue; + + if (nextProps.widget.valueUseMinMax) { + if (this.state.minValue > nextProps.widget.valueMin) { + minValue = nextProps.widget.valueMin; + + this.setState({ minValue }); + this.gauge.setMinValue(minValue); + + updateLabels = true; + } + + if (this.state.maxValue < nextProps.widget.valueMax) { + maxValue = nextProps.widget.valueMax; + + this.setState({ maxValue }); + this.gauge.maxValue = maxValue; + + updateLabels = true; + } + } + + if (updateLabels === false) { + // check if min/max changed + if (minValue > this.gauge.minValue) { + minValue = this.gauge.minValue; + updateLabels = true; + + this.setState({ minValue }); + } + + if (maxValue < this.gauge.maxValue) { + maxValue = this.gauge.maxValue; + updateLabels = true; + + this.setState({ maxValue }); + } + } + + if (updateLabels) { + this.updateLabels(minValue, maxValue); + } // update gauge's value - this.gauge.set(new_value); + this.gauge.set(value); } } } - updateAfterResize(newHeight) { - // Update labels after resize - this.gauge.setOptions({ staticLabels: this.staticLabels(newHeight) }); + updateLabels(minValue, maxValue, force) { + // calculate labels + const labels = []; + const labelCount = 5; + const labelStep = (maxValue - minValue) / (labelCount - 1); + + for (let i = 0; i < labelCount; i++) { + labels.push(minValue + labelStep * i); + } + + // calculate zones + let zones = this.props.widget.colorZones ? this.props.widget.zones : null; + if (zones != null) { + // adapt range 0-100 to actual min-max + const step = (maxValue - minValue) / 100; + + zones = zones.map(zone => { + return Object.assign({}, zone, { min: (zone.min * step) + +minValue, max: zone.max * step + +minValue, strokeStyle: '#' + zone.strokeStyle }); + }); + + console.log(zones); + } + + this.gauge.setOptions({ + staticLabels: { + font: '10px "Helvetica Neue"', + labels, + color: "#000000", + fractionDigits: 1 + }, + staticZones: zones + }); + } + + computeGaugeOptions(widget) { + return { + angle: -0.25, + lineWidth: 0.2, + pointer: { + length: 0.6, + strokeWidth: 0.035 + }, + radiusScale: 0.8, + colorStart: '#6EA2B0', + colorStop: '#6EA2B0', + strokeColor: '#E0E0E0', + highDpiSupport: true, + limitMax: false, + limitMin: false + }; } render() { - var componentClass = this.props.editing ? "gauge-widget editing" : "gauge-widget"; - var signalType = null; + const componentClass = this.props.editing ? "gauge-widget editing" : "gauge-widget"; + let signalType = null; if (this.props.simulation) { - var simulationModel = this.props.simulation.models.filter((model) => model.simulator.node === this.props.widget.simulator.node && model.simulator.simulator === this.props.widget.simulator.simulator)[0]; + const simulationModel = this.props.simulation.models.filter((model) => model.simulator.node === this.props.widget.simulator.node && model.simulator.simulator === this.props.widget.simulator.simulator)[0]; signalType = (simulationModel != null && simulationModel.length > 0) ? simulationModel.mapping[this.props.widget.signal].type : ''; } return ( -
-
{ this.props.widget.name }
- this.gaugeCanvas = node } /> -
{ signalType }
-
{ this.state.value }
+
+
{this.props.widget.name}
+ this.gaugeCanvas = node} /> +
{signalType}
+
{this.state.value}
); } diff --git a/src/styles/app.css b/src/styles/app.css index 18caa91..c042e74 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -247,6 +247,11 @@ body { color: #888; } +.table-control-checkbox input { + position: relative !important; + margin-top: 0 !important; +} + .unselectable { -webkit-touch-callout: none !important; /* iOS Safari */ -webkit-user-select: none !important; /* Safari */