363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
import {
|
|
Avatar,
|
|
Box,
|
|
Card,
|
|
CardHeader,
|
|
Checkbox,
|
|
CircularProgress,
|
|
FormControlLabel,
|
|
FormGroup,
|
|
Grid2 as Grid,
|
|
Theme,
|
|
Typography
|
|
} from "@mui/material";
|
|
import { makeStyles } from "@mui/styles";
|
|
import AreaGraph, { AreaData } from "charts/measurementCharts/AreaGraph";
|
|
import MultiLineGraph, { LineData, Point } from "charts/measurementCharts/MultiLineGraph";
|
|
import UnitMeasurementSummary from "component/UnitMeasurementSummary";
|
|
import { useThemeType } from "hooks";
|
|
import { cloneDeep } from "lodash";
|
|
import { Component } from "models";
|
|
import { UnitMeasurement } from "models/UnitMeasurement";
|
|
import moment, { Moment } from "moment";
|
|
import { GetComponentIcon } from "pbHelpers/ComponentType";
|
|
import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
|
|
import { useGlobalState } from "providers";
|
|
import React, { useState } from "react";
|
|
import { avg } from "utils";
|
|
|
|
interface Props {
|
|
component: Component;
|
|
measurements: UnitMeasurement[];
|
|
isLoading: boolean;
|
|
xDomain: string[] | number[];
|
|
zoomIn: (domain: string[] | number[]) => void;
|
|
}
|
|
|
|
const useStyles = makeStyles((theme: Theme) => {
|
|
return ({
|
|
card: {
|
|
position: "relative",
|
|
display: "flex",
|
|
height: "100%",
|
|
flexDirection: "column",
|
|
overflow: "visible"
|
|
},
|
|
cardHeader: {
|
|
padding: theme.spacing(1),
|
|
paddingLeft: theme.spacing(2),
|
|
marginRight: 10
|
|
},
|
|
avatarIcon: {
|
|
width: 33,
|
|
height: 33
|
|
}
|
|
})
|
|
});
|
|
|
|
export default function BinComponentGraph(props: Props) {
|
|
const { component, measurements, isLoading, xDomain, zoomIn } = props;
|
|
const [selectedNodes, setSelectedNodes] = useState<string[]>(["all"]);
|
|
const themeType = useThemeType();
|
|
const classes = useStyles();
|
|
const [{ user }] = useGlobalState();
|
|
|
|
const handleGrainCableControlsChange = (name: string) => (event: any) => {
|
|
let nodes = selectedNodes;
|
|
if (event.target.checked) {
|
|
if (name === "all" || name === "grain" || name === "air") {
|
|
nodes = [name];
|
|
} else {
|
|
nodes = nodes.filter(item => {
|
|
let str = item.toString();
|
|
return str !== "all" && str !== "air" && str !== "grain";
|
|
});
|
|
nodes.push(name);
|
|
}
|
|
} else {
|
|
if (nodes.includes(name)) {
|
|
nodes = nodes.filter(item => item.toString() !== name.toString());
|
|
if (nodes.length < 1 && name !== "all") {
|
|
nodes = ["all"];
|
|
}
|
|
}
|
|
}
|
|
setSelectedNodes(nodes);
|
|
};
|
|
|
|
const cableControls = (numNodes: number) => {
|
|
if (numNodes <= 1) {
|
|
return null;
|
|
}
|
|
|
|
let grainCableControls = [];
|
|
// let split = isBinSplit(graphFilters);
|
|
// let splitAt = binSplitAt(graphFilters);
|
|
|
|
for (var i = numNodes; i > 0; i--) {
|
|
if (i === numNodes) {
|
|
grainCableControls.push(
|
|
<Grid key="group-selectors">
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={selectedNodes.includes("all") ? true : false}
|
|
onChange={handleGrainCableControlsChange("all")}
|
|
value={"all"}
|
|
disabled={selectedNodes.includes("all") ? true : false}
|
|
/>
|
|
}
|
|
label={"All"}
|
|
/>
|
|
</Grid>
|
|
);
|
|
}
|
|
|
|
let nodeLabel = "Node " + i.toString();
|
|
if (i === 1) {
|
|
nodeLabel = "Bottom";
|
|
} else if (i === numNodes) {
|
|
nodeLabel = "Top";
|
|
}
|
|
grainCableControls.push(
|
|
<Grid key={"node-" + (i - 1).toString()}>
|
|
<FormControlLabel
|
|
//className={split && i <= splitAt ? classes.grainNode : classes.airNode}
|
|
control={
|
|
<Checkbox
|
|
checked={selectedNodes.includes((i - 1).toString()) ? true : false}
|
|
onChange={handleGrainCableControlsChange((i - 1).toString())}
|
|
value={(i - 1).toString()}
|
|
/>
|
|
}
|
|
label={<Typography variant="body2">{nodeLabel}</Typography>}
|
|
/>
|
|
</Grid>
|
|
);
|
|
}
|
|
return numNodes > 0 ? (
|
|
<FormGroup>
|
|
<Grid container direction="row">
|
|
{grainCableControls}
|
|
</Grid>
|
|
</FormGroup>
|
|
) : null;
|
|
};
|
|
|
|
const graphHeader = () => {
|
|
const componentIcon = GetComponentIcon(
|
|
component.settings.type,
|
|
component.settings.subtype,
|
|
themeType
|
|
);
|
|
let nodes = 1;
|
|
let lastMeasurement = cloneDeep(component.status.lastGoodMeasurement);
|
|
if (
|
|
lastMeasurement[0] &&
|
|
lastMeasurement[0].values[0] &&
|
|
lastMeasurement[0].values[0].values.length > 1
|
|
) {
|
|
nodes = lastMeasurement[0].values[0].values.length;
|
|
}
|
|
|
|
return (
|
|
<CardHeader
|
|
avatar={
|
|
<Avatar
|
|
variant="square"
|
|
src={componentIcon}
|
|
className={classes.avatarIcon}
|
|
alt={component.name()}
|
|
/>
|
|
}
|
|
title={
|
|
<React.Fragment>
|
|
<Typography style={{ fontSize: 25, fontWeight: 650 }}>{component.name()}</Typography>
|
|
{nodes > 1 && (
|
|
<Grid container direction="row">
|
|
{cableControls(nodes)}
|
|
</Grid>
|
|
)}
|
|
</React.Fragment>
|
|
}
|
|
subheader={
|
|
<UnitMeasurementSummary
|
|
component={component}
|
|
reading={UnitMeasurement.convertLastMeasurement(
|
|
component.status.lastGoodMeasurement.map(m => UnitMeasurement.create(m, user))
|
|
)}
|
|
/>
|
|
}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const areaGraph = (
|
|
unitMeasurement: UnitMeasurement,
|
|
describer: MeasurementDescriber,
|
|
averages?: number
|
|
) => {
|
|
if (unitMeasurement.timestamps.length > 0) {
|
|
let firstTime: Moment | undefined;
|
|
let lastTime: Moment | undefined;
|
|
//build line data to pass to graph
|
|
let areaData: AreaData[] = [];
|
|
let avgData: AreaData[] = [];
|
|
let maxVals: number[] = [];
|
|
let minVals: number[] = [];
|
|
|
|
unitMeasurement.values.forEach((val, j) => {
|
|
let currentMax = Math.min(...val.values);
|
|
let currentMin = Math.max(...val.values);
|
|
let dataPoint: AreaData = {
|
|
timestamp: moment(unitMeasurement.timestamps[j]).valueOf(),
|
|
value: [currentMax, currentMin]
|
|
};
|
|
if (averages) {
|
|
if (minVals.length === 0) {
|
|
firstTime = moment(unitMeasurement.timestamps[j]);
|
|
}
|
|
maxVals.push(currentMax);
|
|
minVals.push(currentMin);
|
|
if (minVals.length >= averages) {
|
|
lastTime = moment(unitMeasurement.timestamps[j]);
|
|
}
|
|
}
|
|
if (firstTime && lastTime) {
|
|
avgData.push({
|
|
timestamp:
|
|
Math.abs(firstTime.diff(lastTime)) / 2 +
|
|
Math.min(firstTime.valueOf(), lastTime.valueOf()),
|
|
value: [avg(maxVals), avg(minVals)]
|
|
});
|
|
firstTime = undefined;
|
|
lastTime = undefined;
|
|
maxVals = [];
|
|
minVals = [];
|
|
}
|
|
areaData.push(dataPoint);
|
|
});
|
|
return (
|
|
<AreaGraph
|
|
key={unitMeasurement.type}
|
|
customHeight={350}
|
|
data={areaData}
|
|
//averagedData={showAveraged ? avgData : []}
|
|
describer={describer}
|
|
tooltip
|
|
newXDomain={xDomain}
|
|
multiGraphZoom={zoomIn}
|
|
multiGraphZoomOut
|
|
/>
|
|
);
|
|
}
|
|
};
|
|
|
|
const selectedNodeVals = (vals: number[]) => {
|
|
let points: Point[] = [];
|
|
vals.forEach((val, i) => {
|
|
if (selectedNodes.includes("all") || selectedNodes.includes(i.toString())) {
|
|
points.push({ value: val, node: i });
|
|
}
|
|
});
|
|
return points;
|
|
};
|
|
|
|
const lineGraph = (
|
|
unitMeasurement: UnitMeasurement,
|
|
describer: MeasurementDescriber,
|
|
averages?: number
|
|
) => {
|
|
if (unitMeasurement.values.length > 0) {
|
|
let firstTime: Moment | undefined;
|
|
let lastTime: Moment | undefined;
|
|
//build line data to pass to graph
|
|
let lineData: LineData[] = [];
|
|
let averagedData: LineData[] = [];
|
|
let valMap: Map<number, number[]> = new Map<number, number[]>();
|
|
|
|
unitMeasurement.values.forEach((vals, i) => {
|
|
let avgVals: Point[] = [];
|
|
let newLineData: LineData = {
|
|
timestamp: moment(unitMeasurement.timestamps[i]).valueOf(),
|
|
points: selectedNodeVals(vals.values)
|
|
};
|
|
lineData.push(newLineData);
|
|
if (averages) {
|
|
vals.values.forEach((val, k) => {
|
|
let entry = valMap.get(k);
|
|
if (entry) {
|
|
entry.push(val);
|
|
if (entry.length >= averages) {
|
|
lastTime = moment(unitMeasurement.timestamps[i]);
|
|
avgVals.push({
|
|
node: k,
|
|
value: avg(entry)
|
|
});
|
|
}
|
|
} else {
|
|
valMap.set(k, [val]);
|
|
firstTime = moment(unitMeasurement.timestamps[i]);
|
|
}
|
|
});
|
|
}
|
|
if (firstTime && lastTime) {
|
|
averagedData.push({
|
|
points: avgVals,
|
|
timestamp:
|
|
Math.abs(firstTime.diff(lastTime)) / 2 +
|
|
Math.min(firstTime.valueOf(), lastTime.valueOf())
|
|
});
|
|
firstTime = undefined;
|
|
lastTime = undefined;
|
|
valMap = new Map<number, number[]>();
|
|
}
|
|
});
|
|
return (
|
|
<MultiLineGraph
|
|
customHeight={300}
|
|
key={unitMeasurement.type}
|
|
lineData={lineData}
|
|
//averagedData={showAveraged ? averagedData : []}
|
|
numLines={unitMeasurement.values.length > 1 ? unitMeasurement.values[0].values.length : 0}
|
|
describer={describer}
|
|
tooltip
|
|
newXDomain={xDomain}
|
|
multiGraphZoom={zoomIn}
|
|
multiGraphZoomOut
|
|
/>
|
|
);
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card raised key={component.key()} style={{ padding: 10, marginBottom: 15 }}>
|
|
<Box display="flex" justifyContent="center" alignItems="center" style={{ height: 300 }}>
|
|
<CircularProgress size={150} thickness={1.5} />
|
|
</Box>
|
|
</Card>
|
|
);
|
|
} else {
|
|
return (
|
|
<Card raised key={component.key()} style={{ padding: 10, marginBottom: 15 }}>
|
|
{graphHeader()}
|
|
{selectedNodes.includes("all") && component.numNodes() > 1
|
|
? measurements?.map(um =>
|
|
areaGraph(
|
|
um,
|
|
describeMeasurement(um.type, component.type(), component.subType()),
|
|
component.settings.smoothingAverages
|
|
)
|
|
)
|
|
: measurements?.map(um =>
|
|
lineGraph(
|
|
um,
|
|
describeMeasurement(um.type, component.type(), component.subType()),
|
|
component.settings.smoothingAverages
|
|
)
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
}
|