frontend/src/gate/GateFlowGraph.tsx
2025-04-17 15:42:19 -06:00

444 lines
15 KiB
TypeScript

import {
Box,
Button,
Card,
CardHeader,
CircularProgress,
Grid2 as Grid,
LinearProgress,
Typography
} from "@mui/material";
import { teal } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import SingleSetAreaChart, { SSAreaDataPoint } from "charts/SingleSetAreaChart";
import { Gate } from "models/Gate";
import moment, { Moment } from "moment";
import { pond } from "protobuf-ts/pond";
import { useGateAPI, useGlobalState } from "providers";
import React, { useEffect, useState } from "react";
interface Props {
gate: Gate;
device: string | number;
start: Moment;
end: Moment;
ambient?: string;
newXDomain?: number[] | string[];
pressureComponent?: string;
setPCAState: (state: boolean) => void;
multiGraphZoom?: (domain: number[] | string[]) => void;
}
const useStyles = makeStyles(() => ({
calcCard: {
height: 100,
display: "flex",
alignItems: "center"
},
eventCard: {
height: 100,
paddingX: 10,
display: "flex",
alignItems: "center"
},
runtimeGrid: {
marginBottom: 2
},
eventGrid: {
marginTop: 2
}
}));
export default function GateFlowGraph(props: Props) {
const {
gate,
device,
ambient,
pressureComponent,
setPCAState,
start,
end,
newXDomain,
multiGraphZoom
} = props;
const [{as}] = useGlobalState();
const gateAPI = useGateAPI();
const [flowData, setFlowData] = useState<SSAreaDataPoint[]>([]);
const [loadingChartData, setLoadingChartData] = useState(false);
const [recent, setRecent] = useState<SSAreaDataPoint | undefined>();
const [runtime, setRuntime] = useState<moment.Duration>();
const classes = useStyles();
const [flowEvents, setFlowEvents] = useState<pond.AirFlowEvent[]>([]);
const [eventsLoading, setEventsLoading] = useState(false);
const eventThreshold = 5;
const idleFlow = 2.44;
useEffect(() => {
if (loadingChartData) return;
if (ambient && pressureComponent) {
let recent: SSAreaDataPoint | undefined;
setLoadingChartData(true);
gateAPI
.listGateAirflow(
gate.key,
device,
ambient,
pressureComponent,
start.toISOString(),
end.toISOString(),
undefined,
undefined,
as
)
.then(resp => {
let data: SSAreaDataPoint[] = [];
if (resp.data.values) {
let start: Moment | undefined;
let stop: Moment | undefined;
let runtime = 0;
resp.data.values.forEach((val, i) => {
let time = moment(val.time);
let newPoint: SSAreaDataPoint = {
timestamp: time.valueOf(),
value: val.airFlow ?? 0
};
data.push(newPoint);
if (!recent || recent.timestamp < newPoint.timestamp) {
recent = newPoint;
}
/** determine runtime */
// set the start time if the values is greater than the idleFlow and start is not already set
if (val.airFlow >= idleFlow && !start) {
start = time;
}
// set the stop time when off or at the last measurements and the start time is set
if ((val.airFlow < idleFlow || i === resp.data.values.length - idleFlow) && start) {
stop = time;
}
// if both start and stop are set calculate add the timeframe to the total runtime
if (start && stop) {
runtime = runtime + stop.diff(start);
start = undefined;
stop = undefined;
}
});
setRuntime(moment.duration(runtime));
let state = false;
if (
data[data.length - 1].value > gate.lowerFlow() &&
data[data.length - 1].value < gate.upperFlow()
) {
state = true;
}
setPCAState(state);
}
setFlowData(data);
setLoadingChartData(false);
setRecent(recent);
});
}
}, [gateAPI, gate, ambient, pressureComponent, start, end, device, setPCAState, as]); // eslint-disable-line react-hooks/exhaustive-deps
const loadFlowEvents = () => {
if (ambient && pressureComponent) {
setEventsLoading(true);
gateAPI
.listGateFlowEvents(
gate.key,
device,
ambient,
pressureComponent,
start.toISOString(),
end.toISOString(),
idleFlow,
eventThreshold,
undefined,
undefined,
as
)
.then(resp => {
console.log(resp);
setFlowEvents(resp.data.events.map(e => pond.AirFlowEvent.fromObject(e)));
})
.catch(err => {})
.finally(() => {
setEventsLoading(false);
});
}
};
const flowChart = () => {
return (
<Card raised style={{ padding: 10, marginBottom: 15 }}>
<CardHeader
title={<Typography style={{ fontSize: 25, fontWeight: 650 }}>Mass Air Flow</Typography>}
subheader={
recent ? (
<Grid container>
<Grid size={12}>
<span>
{"Mass Flow Rate: "}
<span style={{ color: teal[500], fontWeight: 500 }}>
{recent.value.toFixed(2)} kg/s
</span>
<br />
</span>
</Grid>
<Grid size={12}>
<Typography color="textSecondary" variant={"caption"}>
{moment(recent.timestamp).fromNow()}
</Typography>
</Grid>
</Grid>
) : (
<Typography variant="body1" color="textPrimary">
No Data
</Typography>
)
}
/>
{flowData.length !== 0 ? (
<SingleSetAreaChart
data={flowData}
maxRef={gate.upperFlow()}
minRef={gate.lowerFlow()}
newXDomain={newXDomain}
multiGraphZoom={multiGraphZoom}
/>
) : (
<Box display="flex" flexDirection="column" flexGrow="2">
<div style={{ display: "flex", flexGrow: 2 }}></div>
<Typography variant="subtitle1" color="textSecondary" align="center">
A component may be missing or have no measurements, this data needs the ambient and
pressure components to be set and measuring
</Typography>
<div style={{ display: "flex", flexGrow: 2 }}></div>
</Box>
)}
</Card>
);
};
const eventCards = () => {
let totalEvents = 0;
let eventsInside = 0;
let eventsOutside = 0;
let totalTimeS = 0;
let timeSInside = 0;
let timeSOutside = 0;
flowEvents.forEach(event => {
totalEvents++;
totalTimeS =
totalTimeS + moment.duration(moment(event.start).diff(moment(event.end))).asSeconds();
let avg = 0;
let count = 0;
event.readings.forEach(reading => {
avg = avg + reading.airFlow;
count++;
});
avg = avg / count;
//if the average of the readings for an event are within the range
if (avg < gate.upperFlow() && avg > gate.lowerFlow()) {
eventsInside++;
timeSInside =
timeSInside + moment.duration(moment(event.start).diff(moment(event.end))).asSeconds();
} else {
eventsOutside++;
timeSOutside =
timeSOutside + moment.duration(moment(event.start).diff(moment(event.end))).asSeconds();
}
});
return (
<React.Fragment>
{flowEvents.length > 0 ? (
<Grid container direction="row" spacing={2} className={classes.eventGrid}>
<Grid size={3}>
<Card raised className={classes.eventCard}>
<Grid container alignItems="center">
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Total Events:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{totalEvents}
</Typography>
</Box>
</Grid>
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Total Time:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{moment.duration(totalTimeS, "s").humanize()}
</Typography>
</Box>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.eventCard}>
<Grid container alignItems="center">
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Events Inside:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{eventsInside}
</Typography>
</Box>
</Grid>
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Event Time:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{moment.duration(timeSInside, "s").humanize()}
</Typography>
</Box>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.eventCard}>
<Grid container alignItems="center">
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Events Outside:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{eventsOutside}
</Typography>
</Box>
</Grid>
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Event Time:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{moment.duration(timeSOutside, "s").humanize()}
</Typography>
</Box>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.eventCard}>
<Grid container direction="column" alignItems="center">
<Grid>
<Typography>Performance:</Typography>
</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{(eventsInside / totalEvents) * 100}%
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
</Grid>
) : (
<Box>
{eventsLoading ? (
<Box display="flex" justifyContent="center">
<CircularProgress size={100} />
</Box>
) : (
<Box display="flex" justifyContent="center">
<Button
variant="contained"
color="primary"
onClick={() => {
loadFlowEvents();
}}>
Retrieve Flow Events
</Button>
</Box>
)}
</Box>
)}
</React.Fragment>
);
};
return (
<React.Fragment>
{runtime && (
<Grid container direction="row" spacing={2} className={classes.runtimeGrid}>
<Grid size={3}>
<Card raised className={classes.calcCard}>
<Grid container direction="column" alignItems="center">
<Grid>Approximate Runtime:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{runtime.asHours().toFixed(2)} hr
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.calcCard}>
<Grid container direction="column" alignItems="center">
<Grid>Cost to run PCA:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
$
{parseFloat(
(runtime.asHours() * gate.settings.hourlyPcaCost).toFixed(2)
).toLocaleString()}
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.calcCard}>
<Grid container direction="column" alignItems="center">
<Grid>Cost to run APU:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
$
{parseFloat(
(runtime.asHours() * gate.settings.hourlyApuCost).toFixed(2)
).toLocaleString()}
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.calcCard}>
<Grid container direction="column" alignItems="center">
<Grid>Total Cost:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
$
{parseFloat(
(
runtime.asHours() * gate.settings.hourlyApuCost +
runtime.asHours() * gate.settings.hourlyPcaCost
).toFixed(2)
).toLocaleString()}
</Typography>
</Grid>
<Grid>Estimated Savings:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
$
{parseFloat(
(runtime.asHours() * gate.settings.hourlyApuCost).toFixed(2)
).toLocaleString()}
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
</Grid>
)}
{loadingChartData ? <LinearProgress /> : flowChart()}
{eventCards()}
</React.Fragment>
);
}