built out the graphs for the bin sensor display
This commit is contained in:
parent
b00f10b831
commit
99f37f7bda
3 changed files with 436 additions and 16 deletions
|
|
@ -39,7 +39,6 @@ export default function BinSensorCard(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Card raised sx={{ height: "100%" }}>
|
<Card raised sx={{ height: "100%" }}>
|
||||||
<Box padding={1.5} height="100%" display="flex" flexDirection="column">
|
<Box padding={1.5} height="100%" display="flex" flexDirection="column">
|
||||||
|
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={1}>
|
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={1}>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
{icon &&
|
{icon &&
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { AccessTime, MoreVert } from "@mui/icons-material";
|
import { AccessTime, MoreVert } from "@mui/icons-material";
|
||||||
import { Avatar, Box, Button, Card, Grid2, IconButton, Menu, MenuItem, Typography, useTheme } from "@mui/material";
|
import { Avatar, Box, Button, Card, Grid2, IconButton, Menu, MenuItem, Typography, useTheme } from "@mui/material";
|
||||||
import BinSensorCard from "bin/BinSensorCard";
|
import BinSensorCard from "bin/BinSensorCard";
|
||||||
|
import { GetDefaultDateRange } from "common/time/DateRange";
|
||||||
import { ExtractMoisture } from "grain";
|
import { ExtractMoisture } from "grain";
|
||||||
import { useThemeType } from "hooks";
|
import { useThemeType } from "hooks";
|
||||||
import { Bin, Component } from "models";
|
import { Bin, Component } from "models";
|
||||||
|
|
@ -11,13 +12,15 @@ import { GrainCable } from "models/GrainCable";
|
||||||
import { Headspace } from "models/Headspace";
|
import { Headspace } from "models/Headspace";
|
||||||
import { Plenum } from "models/Plenum";
|
import { Plenum } from "models/Plenum";
|
||||||
import { Pressure } from "models/Pressure";
|
import { Pressure } from "models/Pressure";
|
||||||
import moment from "moment";
|
import moment, { Moment } from "moment";
|
||||||
import { GetComponentIcon } from "pbHelpers/ComponentType";
|
import { GetComponentIcon } from "pbHelpers/ComponentType";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import { quack } from "protobuf-ts/quack";
|
import { quack } from "protobuf-ts/quack";
|
||||||
import { useBinAPI, useComponentAPI, useGlobalState, useSnackbar } from "providers";
|
import { useBinAPI, useComponentAPI, useGlobalState, useSnackbar } from "providers";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { avg } from "utils";
|
import { avg } from "utils";
|
||||||
|
import BinSensorGraph from "./graphs/BinSensorGraph";
|
||||||
|
import TimeBar from "common/time/TimeBar";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bin: Bin
|
bin: Bin
|
||||||
|
|
@ -65,6 +68,9 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
const [selectedComponentSubtype, setSelectedComponentSubtype] = useState(0);
|
const [selectedComponentSubtype, setSelectedComponentSubtype] = useState(0);
|
||||||
const [selectedComponentTopNode, setSelectedComponentTopNode] = useState(0);
|
const [selectedComponentTopNode, setSelectedComponentTopNode] = useState(0);
|
||||||
const [showGraphs, setShowGraphs] = useState(false);
|
const [showGraphs, setShowGraphs] = useState(false);
|
||||||
|
const defaultDateRange = GetDefaultDateRange();
|
||||||
|
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
|
||||||
|
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
|
||||||
|
|
||||||
//organize the components into the correct 'positions' using the bins preferences
|
//organize the components into the correct 'positions' using the bins preferences
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
|
|
@ -320,7 +326,7 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
icon={icon}
|
icon={icon}
|
||||||
lastReading={cable.lastReading}
|
lastReading={cable.lastReading}
|
||||||
name={cable.name()}
|
name={cable.name()}
|
||||||
tag={"cable"}
|
tag={"Cable"}
|
||||||
cableRows={rows}
|
cableRows={rows}
|
||||||
onMenuClick={(event) => {
|
onMenuClick={(event) => {
|
||||||
setComponentUnnassigned(false);
|
setComponentUnnassigned(false);
|
||||||
|
|
@ -670,6 +676,28 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const updateDateRange = (newStartDate: any, newEndDate: any) => {
|
||||||
|
let range = GetDefaultDateRange();
|
||||||
|
range.start = newStartDate;
|
||||||
|
range.end = newEndDate;
|
||||||
|
setStartDate(newStartDate);
|
||||||
|
setEndDate(newEndDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const graphControls = () => {
|
||||||
|
return (
|
||||||
|
<Box display="flex" gap={2} alignItems="center">
|
||||||
|
<Button variant="contained" color="primary" onClick={() => {
|
||||||
|
setShowGraphs(!showGraphs)
|
||||||
|
}}>
|
||||||
|
{showGraphs ? "Show Cards" : "Show Graphs"}
|
||||||
|
</Button>
|
||||||
|
{showGraphs &&
|
||||||
|
<TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} />
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box padding={1}>
|
<Box padding={1}>
|
||||||
|
|
@ -678,14 +706,34 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
<Box>
|
<Box>
|
||||||
<Box display="flex" gap={2} alignItems="center">
|
<Box display="flex" gap={2} alignItems="center">
|
||||||
<Typography sx={{fontWeight: 650, fontSize: 25}}>Sensors</Typography>
|
<Typography sx={{fontWeight: 650, fontSize: 25}}>Sensors</Typography>
|
||||||
<Button variant="contained" color="primary" onClick={() => {
|
{graphControls()}
|
||||||
setShowGraphs(true)
|
|
||||||
}}>
|
|
||||||
Show Graphs
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
{cables.map(cable => {
|
{cables.map(cable => {
|
||||||
|
let device = componentDevices.get(cable.key())
|
||||||
|
if(showGraphs && device){
|
||||||
|
let icon = GetComponentIcon(cable.type(), cable.subType(), themeType)
|
||||||
|
return(
|
||||||
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
|
<BinSensorGraph
|
||||||
|
icon={icon}
|
||||||
|
title={cable.name()}
|
||||||
|
tag={"Cable"}
|
||||||
|
device={device}
|
||||||
|
component={cable.key()}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onMenuClick={(event) => {
|
||||||
|
setComponentUnnassigned(false);
|
||||||
|
setSelectedComponentType(cable.type());
|
||||||
|
setSelectedComponentKey(cable.key());
|
||||||
|
setSelectedComponentTopNode(cable.settings.grainFilledTo);
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
return(
|
return(
|
||||||
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
{cableCard(cable)}
|
{cableCard(cable)}
|
||||||
|
|
@ -694,6 +742,29 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
{plenums.map(plenum => {
|
{plenums.map(plenum => {
|
||||||
|
let device = componentDevices.get(plenum.key())
|
||||||
|
if(showGraphs && device){
|
||||||
|
let icon = GetComponentIcon(plenum.type(), plenum.subType(), themeType)
|
||||||
|
return(
|
||||||
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
|
<BinSensorGraph
|
||||||
|
icon={icon}
|
||||||
|
title={plenum.name()}
|
||||||
|
tag={"Plenum"}
|
||||||
|
device={device}
|
||||||
|
component={plenum.key()}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onMenuClick={(event) => {
|
||||||
|
setComponentUnnassigned(false);
|
||||||
|
setSelectedComponentType(plenum.type());
|
||||||
|
setSelectedComponentKey(plenum.key());
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
return(
|
return(
|
||||||
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
{tempHumidCard(plenum)}
|
{tempHumidCard(plenum)}
|
||||||
|
|
@ -702,6 +773,29 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
{pressures.map(pressure => {
|
{pressures.map(pressure => {
|
||||||
|
let device = componentDevices.get(pressure.key())
|
||||||
|
if(showGraphs && device){
|
||||||
|
let icon = GetComponentIcon(pressure.type(), pressure.subType(), themeType)
|
||||||
|
return(
|
||||||
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
|
<BinSensorGraph
|
||||||
|
icon={icon}
|
||||||
|
title={pressure.name()}
|
||||||
|
tag={"Pressure"}
|
||||||
|
device={device}
|
||||||
|
component={pressure.key()}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onMenuClick={(event) => {
|
||||||
|
setComponentUnnassigned(false);
|
||||||
|
setSelectedComponentType(pressure.type());
|
||||||
|
setSelectedComponentKey(pressure.key());
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
return(
|
return(
|
||||||
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
{pressureCard(pressure)}
|
{pressureCard(pressure)}
|
||||||
|
|
@ -709,6 +803,29 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{ambient.map(ambient => {
|
{ambient.map(ambient => {
|
||||||
|
let device = componentDevices.get(ambient.key())
|
||||||
|
if(showGraphs && device){
|
||||||
|
let icon = GetComponentIcon(ambient.type(), ambient.subType(), themeType)
|
||||||
|
return(
|
||||||
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
|
<BinSensorGraph
|
||||||
|
icon={icon}
|
||||||
|
title={ambient.name()}
|
||||||
|
tag={"Ambient"}
|
||||||
|
device={device}
|
||||||
|
component={ambient.key()}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onMenuClick={(event) => {
|
||||||
|
setComponentUnnassigned(false);
|
||||||
|
setSelectedComponentType(ambient.type());
|
||||||
|
setSelectedComponentKey(ambient.key());
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
return(
|
return(
|
||||||
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
{tempHumidCard(ambient)}
|
{tempHumidCard(ambient)}
|
||||||
|
|
@ -717,6 +834,29 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
{headspaceTH.map(h => {
|
{headspaceTH.map(h => {
|
||||||
|
let device = componentDevices.get(h.key())
|
||||||
|
if(showGraphs && device){
|
||||||
|
let icon = GetComponentIcon(h.type(), h.subType(), themeType)
|
||||||
|
return(
|
||||||
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
|
<BinSensorGraph
|
||||||
|
icon={icon}
|
||||||
|
title={h.name()}
|
||||||
|
tag={"Headspace"}
|
||||||
|
device={device}
|
||||||
|
component={h.key()}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onMenuClick={(event) => {
|
||||||
|
setComponentUnnassigned(false);
|
||||||
|
setSelectedComponentType(h.type());
|
||||||
|
setSelectedComponentKey(h.key());
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
return(
|
return(
|
||||||
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
{tempHumidCard(h)}
|
{tempHumidCard(h)}
|
||||||
|
|
@ -725,6 +865,15 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{headspaceCO2.map(co2 => {
|
{headspaceCO2.map(co2 => {
|
||||||
|
let device = componentDevices.get(co2.key())
|
||||||
|
if(showGraphs && device){
|
||||||
|
let icon = GetComponentIcon(co2.type(), co2.subType(), themeType)
|
||||||
|
return(
|
||||||
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
|
<BinSensorGraph device={device} component={co2.key()} startDate={startDate} endDate={endDate} />
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
return(
|
return(
|
||||||
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
{co2Card(co2)}
|
{co2Card(co2)}
|
||||||
|
|
@ -732,6 +881,14 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{headspaceLidar.map(lidar => {
|
{headspaceLidar.map(lidar => {
|
||||||
|
let device = componentDevices.get(lidar.key())
|
||||||
|
if(showGraphs && device){
|
||||||
|
return(
|
||||||
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
|
<BinSensorGraph device={device} component={lidar.key()} startDate={startDate} endDate={endDate} />
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
{lidarCard(lidar)}
|
{lidarCard(lidar)}
|
||||||
|
|
@ -747,17 +904,33 @@ export default function BinSensorsDisplay(props: Props){
|
||||||
<Typography sx={{fontWeight: 650, fontSize: 25}}>Controllers</Typography>
|
<Typography sx={{fontWeight: 650, fontSize: 25}}>Controllers</Typography>
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
{heaters.map(heater => {
|
{heaters.map(heater => {
|
||||||
|
let device = componentDevices.get(heater.key())
|
||||||
|
if(showGraphs && device){
|
||||||
|
return(
|
||||||
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
|
<BinSensorGraph device={device} component={heater.key()} startDate={startDate} endDate={endDate} />
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
{controllerCard(heater)}
|
{controllerCard(heater)}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{fans.map(fan => {
|
{fans.map(fan => {
|
||||||
|
let device = componentDevices.get(fan.key())
|
||||||
|
if(showGraphs && device){
|
||||||
|
return(
|
||||||
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
|
<BinSensorGraph device={device} component={fan.key()} startDate={startDate} endDate={endDate} />
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
|
||||||
{controllerCard(fan)}
|
{controllerCard(fan)}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
|
|
|
||||||
248
src/bin/graphs/BinSensorGraph.tsx
Normal file
248
src/bin/graphs/BinSensorGraph.tsx
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
import { Avatar, Box, Card, CardHeader, CircularProgress, IconButton, Paper, Typography, useTheme } from "@mui/material";
|
||||||
|
import moment, { Moment } from "moment";
|
||||||
|
import { useComponentAPI, useGlobalState } from "providers";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { UnitMeasurement } from "models/UnitMeasurement";
|
||||||
|
import {
|
||||||
|
Legend,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
TooltipProps,
|
||||||
|
XAxis,
|
||||||
|
YAxis
|
||||||
|
} from "recharts";
|
||||||
|
import { avg, roundTo } from "utils";
|
||||||
|
import { MoreVert } from "@mui/icons-material";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
device: number;
|
||||||
|
icon?: string;
|
||||||
|
title?: string;
|
||||||
|
tag?: string;
|
||||||
|
onMenuClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
component: string;
|
||||||
|
startDate: Moment;
|
||||||
|
endDate: Moment;
|
||||||
|
customHeight?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MeasurementMeta {
|
||||||
|
value: number;
|
||||||
|
unit: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartPoint {
|
||||||
|
timestamp: number;
|
||||||
|
_meta: Record<string, MeasurementMeta>;
|
||||||
|
[seriesKey: string]: number | Record<string, MeasurementMeta> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SeriesInfo {
|
||||||
|
key: string;
|
||||||
|
colour: string;
|
||||||
|
unit: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeValue(value: number, min: number, max: number): number {
|
||||||
|
if (max === min) {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
return ((value - min) / (max - min)) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChartData(measurements: UnitMeasurement[]): {
|
||||||
|
data: ChartPoint[];
|
||||||
|
series: SeriesInfo[];
|
||||||
|
} {
|
||||||
|
const rawSeries = measurements
|
||||||
|
.filter(um => um.timestamps.length > 0 && um.values.length > 0)
|
||||||
|
.map(um => {
|
||||||
|
const points: { timestamp: number; value: number }[] = [];
|
||||||
|
um.values.forEach((valArray, i) => {
|
||||||
|
if (valArray.error || !um.timestamps[i]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nodeValues = valArray.values.filter(v => !isNaN(v));
|
||||||
|
if (nodeValues.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
points.push({
|
||||||
|
timestamp: moment(um.timestamps[i]).valueOf(),
|
||||||
|
value: avg(nodeValues)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const actualValues = points.map(p => p.value);
|
||||||
|
const min = actualValues.length > 0 ? Math.min(...actualValues) : 0;
|
||||||
|
const max = actualValues.length > 0 ? Math.max(...actualValues) : 100;
|
||||||
|
return { um, points, min, max, key: um.label };
|
||||||
|
})
|
||||||
|
.filter(s => s.points.length > 0);
|
||||||
|
|
||||||
|
const timestampMap = new Map<number, ChartPoint>();
|
||||||
|
rawSeries.forEach(({ um, points, min, max, key }) => {
|
||||||
|
points.forEach(({ timestamp, value }) => {
|
||||||
|
let point = timestampMap.get(timestamp);
|
||||||
|
if (!point) {
|
||||||
|
point = { timestamp, _meta: {} };
|
||||||
|
timestampMap.set(timestamp, point);
|
||||||
|
}
|
||||||
|
point[key] = normalizeValue(value, min, max);
|
||||||
|
point._meta[key] = { value, unit: um.unit, label: um.label };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = Array.from(timestampMap.values()).sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
const series = rawSeries.map(({ um, key }) => ({
|
||||||
|
key,
|
||||||
|
colour: um.colour,
|
||||||
|
unit: um.unit,
|
||||||
|
label: um.label
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { data, series };
|
||||||
|
}
|
||||||
|
|
||||||
|
function SensorGraphTooltip(props: TooltipProps<number, string>) {
|
||||||
|
const { active, label, payload } = props;
|
||||||
|
if (!active || !payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" style={{ opacity: 0.9 }}>
|
||||||
|
<Box padding={1}>
|
||||||
|
{payload.map(p => {
|
||||||
|
const dataKey = String(p.dataKey);
|
||||||
|
const meta = (p.payload as ChartPoint)._meta?.[dataKey];
|
||||||
|
return (
|
||||||
|
<Typography color="textPrimary" variant="subtitle1" key={dataKey}>
|
||||||
|
{meta?.label ?? dataKey}
|
||||||
|
{": "}
|
||||||
|
<Box component="span">
|
||||||
|
{meta ? `${roundTo(meta.value, 2)} ${meta.unit}` : p.value}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Typography color="textSecondary" variant="caption">
|
||||||
|
{moment(label).format("lll")}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BinSensorGraph(props: Props) {
|
||||||
|
const { device, icon, title, tag, component, startDate, endDate, customHeight = 350, onMenuClick } = props;
|
||||||
|
const [measurements, setMeasurements] = useState<UnitMeasurement[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const componentAPI = useComponentAPI();
|
||||||
|
const [{ user }] = useGlobalState();
|
||||||
|
const theme = useTheme();
|
||||||
|
const now = moment();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
componentAPI
|
||||||
|
.sampleUnitMeasurements(device, component, startDate, endDate, 1000)
|
||||||
|
.then(res => {
|
||||||
|
setMeasurements(res.data.measurements.map(um => UnitMeasurement.any(um, user)));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setMeasurements([]);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [device, component, startDate, endDate, componentAPI, user]);
|
||||||
|
|
||||||
|
const { data, series } = useMemo(() => buildChartData(measurements), [measurements]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card raised sx={{ height: "100%", padding: 1.5 }}>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={1}>
|
||||||
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
{icon &&
|
||||||
|
<Avatar
|
||||||
|
variant="square"
|
||||||
|
src={icon}
|
||||||
|
alt={name + " icon"}
|
||||||
|
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<Typography fontWeight={500}>{title}</Typography>
|
||||||
|
{tag && <Typography variant="caption" color="textSecondary">- {tag}</Typography>}
|
||||||
|
</Box>
|
||||||
|
{onMenuClick &&
|
||||||
|
<IconButton size="small" onClick={onMenuClick}>
|
||||||
|
<MoreVert fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
<Box height={customHeight} padding={1}>
|
||||||
|
{loading ? (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" height="100%">
|
||||||
|
<CircularProgress size={80} thickness={1.5} />
|
||||||
|
</Box>
|
||||||
|
) : data.length === 0 || series.length === 0 ? (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" height="100%">
|
||||||
|
<Typography color="textSecondary">No measurement data</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<LineChart data={data}>
|
||||||
|
<XAxis
|
||||||
|
allowDataOverflow
|
||||||
|
dataKey="timestamp"
|
||||||
|
domain={["dataMin", "dataMax"]}
|
||||||
|
name="Time"
|
||||||
|
tickFormatter={timestamp => {
|
||||||
|
const t = moment(timestamp);
|
||||||
|
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
|
||||||
|
}}
|
||||||
|
scale="time"
|
||||||
|
type="number"
|
||||||
|
tick={{ fill: theme.palette.text.primary }}
|
||||||
|
stroke={theme.palette.divider}
|
||||||
|
interval="preserveStartEnd"
|
||||||
|
/>
|
||||||
|
{/* <YAxis
|
||||||
|
domain={[0, 100]}
|
||||||
|
tick={false}
|
||||||
|
stroke={theme.palette.divider}
|
||||||
|
label={{
|
||||||
|
value: "Readings",
|
||||||
|
position: "insideLeft",
|
||||||
|
angle: -90,
|
||||||
|
fill: theme.palette.text.primary
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
<Tooltip
|
||||||
|
animationEasing="ease-out"
|
||||||
|
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
|
||||||
|
content={SensorGraphTooltip}
|
||||||
|
/>
|
||||||
|
<Legend verticalAlign="bottom" />
|
||||||
|
{series.map(s => (
|
||||||
|
<Line
|
||||||
|
key={s.key}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={s.key}
|
||||||
|
name={s.label}
|
||||||
|
stroke={s.colour}
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={false}
|
||||||
|
connectNulls
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue