mirror of
https://git.rwth-aachen.de/acs/public/villas/web/
synced 2025-03-09 00:00:01 +01:00
Add gauge widget color zones
This commit is contained in:
parent
7b51cfc3f1
commit
7825d119de
9 changed files with 346 additions and 64 deletions
131
src/components/dialog/edit-widget-color-zones-control.js
Normal file
131
src/components/dialog/edit-widget-color-zones-control.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* File: edit-widget-color-zones-control.js
|
||||
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
******************************************************************************/
|
||||
|
||||
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 <FormGroup>
|
||||
<ControlLabel>Color zones</ControlLabel>
|
||||
|
||||
<Table data={this.state.widget.zones}>
|
||||
<TableColumn width="20" checkbox onChecked={this.checkedCell} />
|
||||
<TableColumn title="Color" dataKey="strokeStyle" inlineEditable onInlineChange={this.changeCell} />
|
||||
<TableColumn title="Minimum" dataKey="min" inlineEditable onInlineChange={this.changeCell} />
|
||||
<TableColumn title="Maximum" dataKey="max" inlineEditable onInlineChange={this.changeCell} />
|
||||
</Table>
|
||||
|
||||
<Button onClick={this.addZone} disabled={!this.props.widget.colorZones}><Glyphicon glyph="plus" /> Add</Button>
|
||||
<Button onClick={this.removeZones} disabled={!this.props.widget.colorZones}><Glyphicon glyph="minus" /> Remove</Button>
|
||||
</FormGroup>;
|
||||
}
|
||||
}
|
||||
|
||||
export default EditWidgetColorZonesControl;
|
|
@ -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(
|
||||
<EditWidgetTextControl key={1} widget={widget} controlId={'name'} label={'Text'} placeholder={'Enter text'} validate={id => validateForm(id)} handleChange={e => handleChange(e)} />,
|
||||
<EditWidgetSimulatorControl key={2} widget={widget} validate={(id) => validateForm(id)} simulation={simulation} handleChange={(e) => gaugeBoundOnChange(e) } />,
|
||||
<EditWidgetSignalControl key={3} widget={widget} validate={(id) => validateForm(id)} simulation={simulation} handleChange={(e) => handleChange(e)} />
|
||||
<EditWidgetSignalControl key={3} widget={widget} validate={(id) => validateForm(id)} simulation={simulation} handleChange={(e) => handleChange(e)} />,
|
||||
<EditWidgetCheckboxControl key={4} widget={widget} controlId="colorZones" text="Show color zones" handleChange={e => handleChange(e)} />,
|
||||
<EditWidgetColorZonesControl key={5} widget={widget} handleChange={e => handleChange(e)} />,
|
||||
<EditWidgetMinMaxControl key={6} widget={widget} controlId="value" handleChange={e => handleChange(e)} />
|
||||
);
|
||||
break;
|
||||
case 'PlotTable':
|
||||
|
|
64
src/components/dialog/edit-widget-min-max-control.js
Normal file
64
src/components/dialog/edit-widget-min-max-control.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* File: edit-widget-min-max-control.js
|
||||
* Author: Markus Grigull <mgrigull@eonerc.rwth-aachen.de>
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
******************************************************************************/
|
||||
|
||||
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 <FormGroup>
|
||||
<ControlLabel>{this.props.label}</ControlLabel>
|
||||
<Checkbox id={this.props.controlId + "UseMinMax"} checked={this.state.widget[this.props.controlId + "UseMinMax"] || ''} onChange={e => this.props.handleChange(e)}>Enable min-max</Checkbox>
|
||||
|
||||
<Table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Min: <FormControl type="number" id={this.props.controlId + "Min"} disabled={!this.state.widget[this.props.controlId + "UseMinMax"]} placeholder="Minimum value" value={this.state.widget[this.props.controlId + 'Min']} onChange={e => this.props.handleChange(e)} />
|
||||
</td>
|
||||
<td>
|
||||
Max: <FormControl type="number" id={this.props.controlId + "Max"} disabled={!this.state.widget[this.props.controlId + "UseMinMax"]} placeholder="Maximum value" value={this.state.widget[this.props.controlId + 'Max']} onChange={e => this.props.handleChange(e)} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</FormGroup>;
|
||||
}
|
||||
}
|
||||
|
||||
export default EditWidgetMinMaxControl;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,9 @@ class TableColumn extends Component {
|
|||
dataIndex: false,
|
||||
inlineEditable: false,
|
||||
clickable: false,
|
||||
labelKey: null
|
||||
labelKey: null,
|
||||
checkbox: false,
|
||||
checkboxKey: ''
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -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(<Button bsClass='table-control-button' onClick={() => child.props.onDelete(index)}><Glyphicon glyph='remove' /></Button>);
|
||||
}
|
||||
|
||||
if (child.props.checkbox) {
|
||||
const checkboxKey = this.props.checkboxKey;
|
||||
|
||||
cell.push(<Checkbox className="table-control-checkbox" inline checked={checkboxKey ? data[checkboxKey] : null} onChange={e => child.props.onChecked(index, e)}></Checkbox>);
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
<div className={ componentClass }>
|
||||
<div className="gauge-name">{ this.props.widget.name }</div>
|
||||
<canvas ref={ (node) => this.gaugeCanvas = node } />
|
||||
<div className="gauge-unit">{ signalType }</div>
|
||||
<div className="gauge-value">{ this.state.value }</div>
|
||||
<div className={componentClass}>
|
||||
<div className="gauge-name">{this.props.widget.name}</div>
|
||||
<canvas ref={node => this.gaugeCanvas = node} />
|
||||
<div className="gauge-unit">{signalType}</div>
|
||||
<div className="gauge-value">{this.state.value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
Loading…
Add table
Reference in a new issue