frontend/src/bin/graphs/BinComponentGraph.tsx

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>
);
}
}