modified the SingleSetAreaChart to take in a few more parameters to be able to customize the tooltip a little better as well as have multicolour for above 0 and below 0 for the new graph on the gate page for showing the delta temp over time
This commit is contained in:
parent
5f44cf6f09
commit
caf9fada15
6 changed files with 235 additions and 9 deletions
|
|
@ -1,9 +1,10 @@
|
|||
import { useTheme } from "@mui/material";
|
||||
import moment from "moment";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Legend,
|
||||
ReferenceArea,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
|
|
@ -13,25 +14,38 @@ import {
|
|||
YAxis
|
||||
} from "recharts";
|
||||
import MaterialChartTooltip from "./MaterialChartTooltip";
|
||||
import { blue, orange } from "@mui/material/colors";
|
||||
import { Payload } from "recharts/types/component/DefaultLegendContent";
|
||||
|
||||
export interface SSAreaDataPoint {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface ColourData {
|
||||
colour: string,
|
||||
label: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: SSAreaDataPoint[];
|
||||
tooltipLabel: string
|
||||
tooltipUnit?: string
|
||||
yAxisLabel: string
|
||||
maxRef?: number;
|
||||
minRef?: number;
|
||||
newXDomain?: number[] | string[];
|
||||
multiGraphZoom?: (domain: number[] | string[]) => void;
|
||||
colourAboveZero?: ColourData
|
||||
colourBelowZero?: ColourData
|
||||
}
|
||||
|
||||
export default function SingleSetAreaChart(props: Props) {
|
||||
const { data, maxRef, minRef, newXDomain, multiGraphZoom } = props;
|
||||
const { data, maxRef, minRef, newXDomain, multiGraphZoom, yAxisLabel, colourAboveZero, colourBelowZero, tooltipLabel, tooltipUnit } = props;
|
||||
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
|
||||
const [refLeft, setRefLeft] = useState<number | undefined>();
|
||||
const [refRight, setRefRight] = useState<number | undefined>();
|
||||
const [legendPayload, setLegendPayload] = useState<Payload[]>([])
|
||||
const theme = useTheme();
|
||||
const now = moment();
|
||||
|
||||
|
|
@ -41,6 +55,40 @@ export default function SingleSetAreaChart(props: Props) {
|
|||
}
|
||||
}, [newXDomain]);
|
||||
|
||||
useEffect(() => {
|
||||
let legend: Payload[] = []
|
||||
if(colourAboveZero){
|
||||
legend.push({
|
||||
value: colourAboveZero.label,
|
||||
color: colourAboveZero.colour,
|
||||
id: colourAboveZero.label,
|
||||
type: "square"
|
||||
})
|
||||
}
|
||||
if(colourBelowZero){
|
||||
legend.push({
|
||||
value: colourBelowZero.label,
|
||||
color: colourBelowZero.colour,
|
||||
id: colourBelowZero.label,
|
||||
type: "square"
|
||||
})
|
||||
}
|
||||
setLegendPayload(legend)
|
||||
}, [colourAboveZero, colourBelowZero])
|
||||
|
||||
const gradientOffset = useMemo(() => {
|
||||
if (!data.length) return 0;
|
||||
|
||||
const values = data.map(p => p.value);
|
||||
const max = Math.max(...values);
|
||||
const min = Math.min(...values);
|
||||
|
||||
if (max <= 0) return 0;
|
||||
if (min >= 0) return 1;
|
||||
|
||||
return max / (max - min);
|
||||
}, [data]);
|
||||
|
||||
const zoom = () => {
|
||||
let newDomain: number[] | string[] = ["dataMin", "dataMax"];
|
||||
if (refLeft && refRight && refLeft !== refRight) {
|
||||
|
|
@ -75,12 +123,16 @@ export default function SingleSetAreaChart(props: Props) {
|
|||
setRefRight(undefined);
|
||||
zoom();
|
||||
}}>
|
||||
<Legend
|
||||
verticalAlign="top"
|
||||
payload={legendPayload}
|
||||
/>
|
||||
<Tooltip
|
||||
animationEasing="ease-out"
|
||||
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
|
||||
labelFormatter={timestamp => moment(timestamp).format("lll")}
|
||||
content={(props: TooltipProps<any, any>) => (
|
||||
<MaterialChartTooltip {...props} valueFormatter={value => `${value}`} />
|
||||
<MaterialChartTooltip {...props} valueFormatter={value => `${value}` + (tooltipUnit ? tooltipUnit : "")} />
|
||||
)}
|
||||
/>
|
||||
<XAxis
|
||||
|
|
@ -102,13 +154,26 @@ export default function SingleSetAreaChart(props: Props) {
|
|||
type="number"
|
||||
domain={["auto", "auto"]}
|
||||
label={{
|
||||
value: "Mass Flow (kg/s)",
|
||||
value: yAxisLabel,
|
||||
position: "insideLeft",
|
||||
angle: -90,
|
||||
fill: theme.palette.text.primary
|
||||
}}
|
||||
/>
|
||||
<Area dataKey={"value"} strokeWidth={3} />
|
||||
<defs>
|
||||
{colourAboveZero && colourBelowZero &&
|
||||
<linearGradient id="solidColour" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset={gradientOffset} stopColor={colourAboveZero.colour} stopOpacity="75%" />
|
||||
<stop offset={gradientOffset} stopColor={colourBelowZero.colour} stopOpacity="75%" />
|
||||
</linearGradient>
|
||||
}
|
||||
</defs>
|
||||
<Area dataKey={"value"} strokeWidth={3}
|
||||
type="monotone"
|
||||
name={tooltipLabel}
|
||||
stroke={(colourAboveZero && colourBelowZero) && "url(#solidColour)"}
|
||||
fill={(colourAboveZero && colourBelowZero) && "url(#solidColour)"}
|
||||
/>
|
||||
<ReferenceLine
|
||||
ifOverflow="extendDomain"
|
||||
y={maxRef}
|
||||
|
|
|
|||
152
src/gate/GateDeltaTempGraph.tsx
Normal file
152
src/gate/GateDeltaTempGraph.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { Box, Card, CardHeader, Grid2, Typography } from "@mui/material"
|
||||
import { blue, orange } from "@mui/material/colors"
|
||||
import SingleSetAreaChart, { SSAreaDataPoint } from "charts/SingleSetAreaChart"
|
||||
import { useComponentAPI } from "hooks"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { Component } from "models"
|
||||
import { UnitMeasurement } from "models/UnitMeasurement"
|
||||
import moment from "moment"
|
||||
import { Moment } from "moment"
|
||||
import { pond } from "protobuf-ts/pond"
|
||||
import { quack } from "protobuf-ts/quack"
|
||||
import { useGlobalState } from "providers"
|
||||
import { useEffect, useState } from "react"
|
||||
import { getTemperatureUnit } from "utils"
|
||||
|
||||
|
||||
interface Props {
|
||||
//the key for the temperature chain, this may be overhauled to only be a single node in the future for the outlet and the 'inlet' would be the ambient
|
||||
deviceID: number
|
||||
tempComponent: Component
|
||||
ambient?: Component
|
||||
start: Moment;
|
||||
end: Moment;
|
||||
}
|
||||
|
||||
export default function GateDeltaTempGraph(props: Props){
|
||||
const {
|
||||
tempComponent,
|
||||
ambient,
|
||||
deviceID,
|
||||
start,
|
||||
end
|
||||
} = props
|
||||
const [data, setData] = useState<SSAreaDataPoint[]>([])
|
||||
const [{user}] = useGlobalState()
|
||||
const componentAPI = useComponentAPI();
|
||||
const [recent, setRecent] = useState<SSAreaDataPoint>()
|
||||
const warmingColour = orange["500"]
|
||||
const coolingColour = blue["500"]
|
||||
|
||||
/**
|
||||
* the use effect will use the temp component passed in get the measurements, then use the first node and the last node to determine the difference,
|
||||
* if there is only one node, or only one node is reading due to errors, then it will use that as both the inlet and outlet so the delta will be 0,
|
||||
*
|
||||
* //NOT IMPLEMENTED YET//
|
||||
* if an ambient is passed in it will use the readings from the ambient as the inlet and the temp component as the outlet
|
||||
*/
|
||||
useEffect(()=>{
|
||||
//get the measurements for the temp component
|
||||
componentAPI.listUnitMeasurements(
|
||||
deviceID,
|
||||
tempComponent.key(),
|
||||
start.toISOString(),
|
||||
end.toISOString(),
|
||||
500,
|
||||
0,
|
||||
"timestamp",
|
||||
).then(resp => {
|
||||
if(resp.data.measurements){
|
||||
let tempCompMeasurements = resp.data.measurements.map(um => UnitMeasurement.any(um, user));
|
||||
//if there is an ambient component, then treat the temp component like a single sensor and not a chain, and use the ambient measurements as the inlet
|
||||
if(ambient){
|
||||
//TODO: this is just an idea of how to handle a possible path for the overhaul of gates, if that is the route we take then this needs to be coded
|
||||
|
||||
}else{//else no ambient is passed in treat the temp component like a chain
|
||||
let deltas: SSAreaDataPoint[] = []
|
||||
tempCompMeasurements.forEach(um => {
|
||||
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE){
|
||||
um.values.forEach((nodes, index) => {
|
||||
if (nodes.values && nodes.values.length > 0 && um.timestamps[index]){
|
||||
//use the first node as the inlet
|
||||
let inletNode = nodes.values[0]
|
||||
//use the second node as the outlet, if there is an error or there is only one node then it will be used as both and the delta will be 0
|
||||
let outletNode = nodes.values[nodes.values.length -1]
|
||||
let newDelta: SSAreaDataPoint = {
|
||||
value: Math.round((outletNode - inletNode)*100)/100,
|
||||
timestamp: moment(um.timestamps[index]).valueOf(),
|
||||
}
|
||||
deltas.push(newDelta)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
setData(deltas)
|
||||
//use the components status to set the recent to show in the header of the card
|
||||
let recent: SSAreaDataPoint | undefined
|
||||
if(tempComponent.status.measurement){
|
||||
tempComponent.status.measurement.forEach(um => {
|
||||
if(um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE){
|
||||
let clone = cloneDeep(um)
|
||||
let m = UnitMeasurement.create(clone, user)
|
||||
if(m.values.length > 0){
|
||||
let lastReading = m.values[m.values.length - 1]
|
||||
if (lastReading.values.length > 0){
|
||||
recent = {
|
||||
value: lastReading.values[lastReading.values.length -1] - lastReading.values[0],
|
||||
timestamp: moment(m.timestamps[m.values.length - 1]).valueOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
setRecent(recent)
|
||||
}
|
||||
}
|
||||
})
|
||||
},[tempComponent, ambient, deviceID, start, end])
|
||||
|
||||
return (
|
||||
<Card raised style={{ padding: 10, marginBottom: 15, marginTop: 15 }}>
|
||||
<CardHeader
|
||||
title={<Typography style={{ fontSize: 25, fontWeight: 650 }}>Delta Temp</Typography>}
|
||||
subheader={
|
||||
recent ?
|
||||
(
|
||||
<Grid2 container>
|
||||
<Grid2 size={12}>
|
||||
<span>
|
||||
{"Delta Temp: "}
|
||||
<span style={{ color: recent.value > 0 ? warmingColour : coolingColour, fontWeight: 500 }}>
|
||||
{recent.value.toFixed(2) + (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C")}
|
||||
</span>
|
||||
<br />
|
||||
</span>
|
||||
</Grid2>
|
||||
<Grid2 size={12}>
|
||||
<Typography color="textSecondary" variant={"caption"}>
|
||||
{moment(recent.timestamp).fromNow()}
|
||||
</Typography>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
)
|
||||
: (
|
||||
<Typography variant="body1" color="textPrimary">
|
||||
No Data
|
||||
</Typography>
|
||||
)
|
||||
|
||||
}
|
||||
/>
|
||||
<SingleSetAreaChart
|
||||
data={data}
|
||||
tooltipLabel="Delta Temp"
|
||||
tooltipUnit={getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C"}
|
||||
yAxisLabel={"Delta T" + (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C")}
|
||||
colourAboveZero={{colour: warmingColour, label: "Heating"}}
|
||||
colourBelowZero={{colour: coolingColour, label: "Cooling"}}/>
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -209,6 +209,8 @@ export default function GateFlowGraph(props: Props) {
|
|||
{flowData.length !== 0 ? (
|
||||
<SingleSetAreaChart
|
||||
data={flowData}
|
||||
tooltipLabel="Flow"
|
||||
yAxisLabel="Mass Flow (kg/s)"
|
||||
maxRef={gate.upperFlow()}
|
||||
minRef={gate.lowerFlow()}
|
||||
newXDomain={newXDomain}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@ import { avg } from "utils";
|
|||
import GateFlowGraph from "./GateFlowGraph";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { cloneDeep } from "lodash";
|
||||
import GateDeltaTempGraph from "./GateDeltaTempGraph";
|
||||
|
||||
interface Props {
|
||||
gate: Gate;
|
||||
display: "analytics" | "sensors";
|
||||
compMap: Map<string, Component>;
|
||||
device: string | number;
|
||||
device: number;
|
||||
pressure: string;
|
||||
ambient: string;
|
||||
tempChain: string;
|
||||
|
|
@ -297,7 +298,7 @@ export default function GateGraphs(props: Props) {
|
|||
}
|
||||
|
||||
/**
|
||||
* This function creates the charts for the gate page using the MAIN components on the gate, it DOES NOT show any other components that could be on the device
|
||||
* This function creates the charts for the gate page using the MAIN components on the gate, and delta time, it DOES NOT show any other components that could be on the device
|
||||
* @returns list of cards for the charts
|
||||
*/
|
||||
const sensorGraphs = () => {
|
||||
|
|
@ -309,6 +310,8 @@ export default function GateGraphs(props: Props) {
|
|||
if(pressureComp && pressureReadings){
|
||||
graphs.push(graphCard(pressureComp, pressureReadings))
|
||||
}
|
||||
// maybe also put the delta time graph here as well
|
||||
|
||||
//T/H chain
|
||||
let tempComp = compMap.get(tempChain)
|
||||
let tempReadings = compMeasurements.get(tempChain)
|
||||
|
|
@ -337,6 +340,7 @@ export default function GateGraphs(props: Props) {
|
|||
}
|
||||
|
||||
const analyticGraphs = () => {
|
||||
let tempComp = compMap.get(tempChain)
|
||||
return (
|
||||
<Box>
|
||||
<GateFlowGraph
|
||||
|
|
@ -355,6 +359,10 @@ export default function GateGraphs(props: Props) {
|
|||
setZoomed(true);
|
||||
}}
|
||||
/>
|
||||
{/* add a new chart here for the delta temp over time */}
|
||||
{tempComp &&
|
||||
<GateDeltaTempGraph deviceID={device} start={startDate} end={endDate} tempComponent={tempComp}/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ export default function GateList(props: Props) {
|
|||
<Typography>{terminalMap.get(gate.terminal()) ?? "None"}</Typography>
|
||||
</Box>
|
||||
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
|
||||
<Typography>Conditioning:</Typography>
|
||||
<Typography>Delta Temp:</Typography>
|
||||
{conditionDisplay(gate)}
|
||||
</Box>
|
||||
{/* taking these out for now because we are not sure if the customer wants them */}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@ export default function Gate(props: Props) {
|
|||
gateAPI
|
||||
.getGatePageData(id, as)
|
||||
.then(resp => {
|
||||
console.log(resp.data);
|
||||
let p = new Map<number, pond.GateDeviceType>();
|
||||
Object.keys(resp.data.preferences).forEach(k => {
|
||||
let prefKey = parseInt(k);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue