Completes main page

This commit is contained in:
Patrick McDonagh
2018-04-17 16:05:48 -05:00
parent 9f97e05a22
commit 75b0a49ce1
14 changed files with 2005 additions and 281 deletions

View File

@@ -0,0 +1,154 @@
import React, { Component } from "react";
import _ from "lodash";
import moment from "moment";
import { FlexibleWidthXYPlot, LineSeries, VerticalGridLines, HorizontalGridLines, XAxis, YAxis, DiscreteColorLegend, Crosshair} from "react-vis";
import "../../../node_modules/react-vis/dist/style.css";
export function findClosestXValue(tagHistory, xval){
const mapped = _.map(tagHistory, ({timestamp, value}) => {
const timeInt = timestamp.getTime();
return { dist: Math.abs(xval - timeInt), x: xval, y: value };
});
const sorted = _.orderBy(mapped, "dist", "asc");
return(_.omit(sorted[0], "dist"));
}
/**
* Map an array of objects to a graph-usable array of objects
*
* @param {Array} values - list of objects with timestame and value properties
*
* @returns {Array} list of objects with x and y properties
*/
export const mapTimestampAndValuePropToXY = (values) => {
return _.map(values, ({timestamp, value}) => {
return {x: (new Date(timestamp)).getTime(), y: value};
});
};
export class FlexibleGraph extends Component{
constructor(props){
super(props);
// props.tagHistory
// props.tags
// props.tagDescriptions
// props.units
// props.round
this.state = {
crosshairValues: []
};
}
tagsExist = () => {
if (!this.props.tags){
return false;
}
_.forEach(this.props.tags, (tag) =>{
if (!tag || !tag.value){
return false;
}
});
_.forEach(this.props.tagHistory, (tag) => {
if( !tag || !tag.value) {
return false;
}
});
return true;
}
renderLegendData = () =>{
if (!this.tagsExist()){
return _.map(this.props.tagDescriptions, (desc) => {
return {title: desc};
});
} else {
return _.map(this.props.tagDescriptions, (desc, key) => {
return {title: `${desc}: ${_.round(this.props.tags[key].value, this.props.round[key])} ${this.props.units[key]}`};
});
}
}
renderCrossHairData = (values) => {
const mappedValues = _.map(this.props.tagDescriptions, (desc, key) => {
return {title: desc, value: _.round(values[key].y, 1) + ` ${this.props.units[key]}`};
});
console.log(mappedValues, this.props.tagDescriptions);
return mappedValues;
}
renderTitleData = (values) => {
return {title: "Time", value: moment(values[0].x).format("hh:mm A")};
}
/**
* Event handler for onMouseLeave.
* @private
*/
_onMouseLeave = () => {
this.setState({crosshairValues: []});
}
/**
* Event handler for onNearestX.
* @param {Object} value Selected value.
* @param {index} index Index of the value in the data array.
* @private
*/
_onNearestX = (value) => {
this.setState({
crosshairValues: _.map(this.props.tagHistory, (tagHist) => {
return findClosestXValue(tagHist, value.x);
})
});
}
render(){
const lineSeries = _.map(this.props.tagHistory, (tag, key) => {
if(key === 0){
return <LineSeries key={key} data={mapTimestampAndValuePropToXY(tag)} onNearestX={this._onNearestX} />;
} else {
return <LineSeries key={key} data={mapTimestampAndValuePropToXY(tag)} />;
}
});
if(!this.tagsExist()){
return <span className="flexible-graph-notags"></span>;
}
return(
<div className="flexible-graph">
<FlexibleWidthXYPlot
height={300}
xType="time"
onMouseLeave={this._onMouseLeave}
>
<VerticalGridLines />
<HorizontalGridLines />
<XAxis />
<YAxis />
{lineSeries}
<Crosshair
values={this.state.crosshairValues}
titleFormat={this.renderTitleData}
itemsFormat = {this.renderCrossHairData}
/>
</FlexibleWidthXYPlot>
<DiscreteColorLegend
items={this.renderLegendData()}
orientation="horizontal"
/>
</div>
);
}
}

View File

@@ -0,0 +1,86 @@
import React from "react";
import { color } from "d3-color";
import { interpolateRgb } from "d3-interpolate";
import LiquidFillGauge from "react-liquid-gauge";
export function renderLabel(props, inputVal){
const value = Math.round(parseFloat(inputVal));
const radius = Math.min(props.height / 2, props.width / 2);
const textPixels = (props.textSize * radius / 2);
const valueStyle = {
fontSize: textPixels
};
const percentStyle = {
fontSize: textPixels * 0.6
};
return (
<tspan>
<tspan className="value" style={valueStyle}>{value}</tspan>
<tspan style={percentStyle}>{props.percent}</tspan>
</tspan>
);
}
export function LiquidGauge(props){
// tag, units, maxValue, label
if (!props.tag){
return <span className="liquidgauge-no-tags"></span>;
}
const tagValue = props.tag.value;
const endColor = "#1F4788";
const startColor = "#dc143c";
const radius = 100;
const interpolate = interpolateRgb(startColor, endColor);
const fillColor = interpolate(tagValue / props.maxValue);
const gradientStops = [
{
key: "0%",
stopColor: color(fillColor).darker(0.5).toString(),
stopOpacity: 1,
offset: "0%"
},
{
key: "50%",
stopColor: fillColor,
stopOpacity: 0.75,
offset: "50%"
},
{
key: "100%",
stopColor: color(fillColor).brighter(0.5).toString(),
stopOpacity: 0.5,
offset: "100%"
}
];
return (
<div className={"col liquidfill lqdfill-" + props.tag.name} style={{textAlign: "center"}}>
<h3>{props.label}</h3>
<LiquidFillGauge
style={{ margin: "0 auto" }}
width={radius * 2}
height={radius * 2}
value={tagValue / props.maxValue * 100}
percent={props.units}
textSize={1}
textOffsetX={0}
textOffsetY={0}
textRenderer={(props) => renderLabel(props, tagValue)}
riseAnimation
waveAnimation
waveFrequency={3}
waveAmplitude={2}
gradient
gradientStops={gradientStops}
circleStyle={{fill: fillColor}}
waveStyle={{fill: fillColor}}
textStyle={{fill: color("#444").toString(), fontFamily: "Arial"}}
waveTextStyle={{fill: color("#fff").toString(), fontFamily: "Arial"}}
/>
</div>
);
}

View File

@@ -1,128 +1,16 @@
import React, { Component } from "react";
import _ from "lodash";
import { connect } from "react-redux";
import { FlexibleWidthXYPlot, LineSeries, VerticalGridLines, HorizontalGridLines, XAxis, YAxis, DiscreteColorLegend } from "react-vis";
import "../../../node_modules/react-vis/dist/style.css";
import { color } from "d3-color";
import { interpolateRgb } from "d3-interpolate";
import LiquidFillGauge from "react-liquid-gauge";
import { LiquidGauge } from "./LiquidGauge";
import { FlexibleGraph } from "./FlexibleGraph";
// const graphColors = ["#d7191c", "#fdae61", "#ffffbf", "#abd9e9", "#2c7bb6"];
/** Class for Main Page
*
* @extends React.Component
*/
export class Main extends Component {
/**
* Map an array of objects to a graph-usable array of objects
*
* @param {Array} values - list of objects with timestame and value properties
*
* @returns {Array} list of objects with x and y properties
*/
mapTimestampAndValuePropToXY(values){
return _.map(values, (val) => {
return {x: val.timestamp, y: val.value};
});
}
/**
* Renders a Liquid Gauge.
*
* @param {object} tag - the tag structure
* @param {string} units - units for the tag
* @param {number} maxValue - maximum value to be displayed
* @param {number} label - label for the value (typically the tag name)
* @returns {ReactElement} liquidGauge
*/
renderLiquidGauge(tag, units, maxValue, label){
if (!tag){
return <span></span>;
}
const tagValue = tag.value;
const endColor = "#1F4788";
const startColor = "#dc143c";
const radius = 100;
const interpolate = interpolateRgb(startColor, endColor);
const fillColor = interpolate(tagValue / maxValue);
const gradientStops = [
{
key: "0%",
stopColor: color(fillColor).darker(0.5).toString(),
stopOpacity: 1,
offset: "0%"
},
{
key: "50%",
stopColor: fillColor,
stopOpacity: 0.75,
offset: "50%"
},
{
key: "100%",
stopColor: color(fillColor).brighter(0.5).toString(),
stopOpacity: 0.5,
offset: "100%"
}
];
return (
<div className={"col liquidfill lqdfill-" + tag.name} style={{textAlign: "center"}}>
<h3>{label}</h3>
<LiquidFillGauge
style={{ margin: "0 auto" }}
width={radius * 2}
height={radius * 2}
value={tagValue / maxValue * 100}
percent={units}
textSize={1}
textOffsetX={0}
textOffsetY={0}
textRenderer={(props) => {
const value = Math.round(tagValue);
const radius = Math.min(props.height / 2, props.width / 2);
const textPixels = (props.textSize * radius / 2);
const valueStyle = {
fontSize: textPixels
};
const percentStyle = {
fontSize: textPixels * 0.6
};
return (
<tspan>
<tspan className="value" style={valueStyle}>{value}</tspan>
<tspan style={percentStyle}>{props.percent}</tspan>
</tspan>
);
}}
riseAnimation
waveAnimation
waveFrequency={3}
waveAmplitude={2}
gradient
gradientStops={gradientStops}
circleStyle={{
fill: fillColor
}}
waveStyle={{
fill: fillColor
}}
textStyle={{
fill: color("#444").toString(),
fontFamily: "Arial"
}}
waveTextStyle={{
fill: color("#fff").toString(),
fontFamily: "Arial"
}}
/>
</div>
);
}
/**
* render
@@ -130,7 +18,8 @@ export class Main extends Component {
* @returns {ReactElement} markup
*/
render(){
if (!this.props.tagHistory){
if (!this.props.tagHistory || Object.keys(this.props.tagHistory).length === 0){
return(
<div className="container loading">
<h1>Loading...</h1>
@@ -147,7 +36,25 @@ export class Main extends Component {
);
}
if (Object.keys(this.props.tags).length === 0){
if (!this.props.tags ||
!this.props.tagHistory ||
!this.props.tags.val_FluidLevel ||
!this.props.tags.val_Flowmeter_BarrelsPerDay ||
!this.props.tags.val_TubingPressure ||
!this.props.tags.VFD_SpeedFdbk ||
!this.props.tags.VFD_OutCurrent ||
!this.props.tags.VFD_OutPower ||
!this.props.tags.VFD_Temp ||
!this.props.tagHistory.val_FluidLevel ||
!this.props.tagHistory.val_Flowmeter_BarrelsPerDay ||
!this.props.tagHistory.val_IntakePressure ||
!this.props.tagHistory.val_IntakeTemperature ||
!this.props.tagHistory.val_TubingPressure ||
!this.props.tagHistory.VFD_SpeedFdbk ||
!this.props.tagHistory.VFD_OutCurrent ||
!this.props.tagHistory.VFD_OutPower ||
!this.props.tagHistory.VFD_Temp
){
return(
<div className="container waiting">
<h1>Waiting for data...</h1>
@@ -155,69 +62,81 @@ export class Main extends Component {
);
}
return (
<div className="container main">
<h3>Process Values</h3>
<div className="row" style={{marginBottom: "20px"}}>
{this.renderLiquidGauge(this.props.tags.val_FluidLevel, "ft.", 500, "Level")}
{this.renderLiquidGauge(this.props.tags.val_Flowmeter_BarrelsPerDay, "BPD", 5000, "Flow Rate")}
{this.renderLiquidGauge(this.props.tags.val_TubingPressure, "PSI", 400, "Tubing Pressure")}
<LiquidGauge tag={this.props.tags.val_FluidLevel} units="ft." maxValue={500} label="Level" />
<LiquidGauge tag={this.props.tags.val_Flowmeter_BarrelsPerDay} units="BPD" maxValue={5000} label="Flow Rate" />
<LiquidGauge tag={this.props.tags.val_TubingPressure} units="PSI" maxValue={400} label="Tubing Pressure" />
</div>
<FlexibleWidthXYPlot
height={300}
xType="time"
>
<VerticalGridLines />
<HorizontalGridLines />
<XAxis />
<YAxis />
<LineSeries data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_IntakePressure)} />
<LineSeries data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_Flowmeter)} />
<LineSeries data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_FluidLevel)} />
<LineSeries data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_IntakeTemperature)} />
<LineSeries data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.val_TubingPressure)} />
</FlexibleWidthXYPlot>
<DiscreteColorLegend
items={[
{title: "Intake Pressure"},
{title: "Flowmeter"},
{title: "Fluid Level"},
{title: "Intake Temp"},
{title: "Tubing Pressure"}
<FlexibleGraph
tagHistory={[
this.props.tagHistory.val_FluidLevel,
this.props.tagHistory.val_Flowmeter,
this.props.tagHistory.val_IntakePressure,
this.props.tagHistory.val_IntakeTemperature,
this.props.tagHistory.val_TubingPressure
]}
orientation="horizontal"
tags={[
this.props.tags.val_FluidLevel,
this.props.tags.val_Flowmeter,
this.props.tags.val_IntakePressure,
this.props.tags.val_IntakeTemperature,
this.props.tags.val_TubingPressure
]}
tagDescriptions={[
"Level",
"Flow Rate",
"Intake Pres",
"Intake Temp",
"Tubing Pres"
]}
units={[
"Ft.",
"GPM",
"PSI",
"deg F",
"PSI"
]}
round={[1, 2, 1, 1, 1]}
/>
<hr />
<h3>VFD Data</h3>
<div className="row" style={{marginBottom: "20px"}}>
{this.renderLiquidGauge(this.props.tags.VFD_OutCurrent, "A.", 100, "Current")}
{this.renderLiquidGauge(this.props.tags.VFD_SpeedFdbk, "Hz", 60, "Frequency")}
<LiquidGauge tag={this.props.tags.VFD_OutCurrent} units="A." maxValue={100} label="Current" />
<LiquidGauge tag={this.props.tags.VFD_SpeedFdbk} units="Hz" maxValue={60} label="Frequency" />
</div>
<FlexibleWidthXYPlot
height={300}
xType="time"
>
<VerticalGridLines />
<HorizontalGridLines />
<XAxis />
<YAxis />
<LineSeries data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.VFD_OutCurrent)} />
<LineSeries data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.VFD_SpeedFdbk)} />
<LineSeries data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.VFD_OutPower)} />
<LineSeries data={this.mapTimestampAndValuePropToXY(this.props.tagHistory.VFD_Temp)} />
</FlexibleWidthXYPlot>
<DiscreteColorLegend
items={[
{title: "VFD Current"},
{title: "VFD Speed Feedback"},
{title: "VFD Output Power"},
{title: "VFD Temp"}
<FlexibleGraph
tagHistory={[
this.props.tagHistory.VFD_SpeedFdbk,
this.props.tagHistory.VFD_OutCurrent,
this.props.tagHistory.VFD_OutPower,
this.props.tagHistory.VFD_Temp
]}
orientation="horizontal"
/>
tags={[
this.props.tags.VFD_SpeedFdbk,
this.props.tags.VFD_OutCurrent,
this.props.tags.VFD_OutPower,
this.props.tags.VFD_Temp
]}
tagDescriptions={[
"Speed",
"Current",
"Power",
"Temp"
]}
units={[
"Hz",
"Amps",
"kW",
"deg C"
]}
round={[2, 2, 1, 1]}
/>
</div>
);
}

View File

@@ -77,18 +77,18 @@ export class Settings extends Component {
return (
<div className="container settings">
<h1>Settings</h1>
<form className="form-inline mb-2">
<form className="form-inline">
<div className="form-group">
<label htmlFor="ipAddress">IP Address</label>
<input
id="ipAddress"
className="form-control ip-address-field"
className="form-control ip-address-field m-2"
value={this.state.ipAddress}
placeholder="PLC IP Address"
onChange={this.onIpAddressInputChange}
/>
<button
className={ipAddressBtnClass + " btn btn-primary ip-submit-button"}
className={ipAddressBtnClass + " btn btn-primary ip-submit-button m-2"}
onClick={(e) => this.sendIpAddress(e)}>
Set IP Address
</button>
@@ -100,23 +100,35 @@ export class Settings extends Component {
<ul className="list-group tag-list">
{this.getTagList()}
</ul>
<form>
<input
value={this.state.newTag}
onChange={this.onNewTagChange}
placeholder="New Tag Name..."
className="tag-name-input"
/>
<button
className="btn btn-success float-right add-tag-button"
onClick={(e) => this.onNewTagSubmit(e)}
>Add Tag</button>
<hr />
<form className="form-inline">
<div className="form-group">
<label htmlFor="tag-name-input">New Tag</label>
<input
value={this.state.newTag}
onChange={this.onNewTagChange}
placeholder="New Tag Name..."
className="tag-name-input form-control m-2"
id="tag-name-input"
/>
<button
className="btn btn-success float-right add-tag-button m-2"
onClick={(e) => this.onNewTagSubmit(e)}
>Add Tag</button>
</div>
</form>
<hr />
<div>
<button
className={initializeBtnClass + " btn btn-success save-button"}
onClick={(e) => this.onSave(e)}
>Save</button>
</form>
</div>
<hr />
</div>
);
}

View File

@@ -2,6 +2,8 @@ import _ from "lodash";
import { IPC_TAGUPDATE } from "../actions/actions_tags";
import {history_tags as historyTags} from "../../../tagList.json";
const historyPoints = 50000;
export default function(state = {}, action){
switch (action.type) {
@@ -14,7 +16,7 @@ export default function(state = {}, action){
};
let tagHistory = [ thisEntry ];
if (state[name]){
tagHistory = _.take(_.concat(tagHistory, state[name]), 500);
tagHistory = _.take(_.concat(tagHistory, state[name]), historyPoints);
}
return { ...state, [name]: tagHistory};

View File

@@ -20,6 +20,8 @@ import { ipcTagUpdate } from "./actions/actions_tags";
import { ipcPlcDetailsReceived, ipcPlcErrorReceived } from "./actions/actions_plc";
export const { history_tags: historyTags, event_tags: eventTags } = require("../../tagList.json");
export const historyPoints = 5000;
const ipc = createIpc({
"tag:valueupdate": ipcTagUpdate,