frontend/src/charts/GrainDryingChart.tsx

200 lines
6 KiB
TypeScript

import { useTheme } from "@mui/material";
import { blue, indigo, orange, red } from "@mui/material/colors";
import MaterialChartTooltip from "charts/MaterialChartTooltip";
import { usePrevious } from "hooks";
import moment from "moment";
import { useEffect, useState } from "react";
import {
Area,
AreaChart,
Legend,
ReferenceArea,
ResponsiveContainer,
Tooltip,
TooltipProps,
XAxis,
YAxis
} from "recharts";
import { roundTo } from "utils/numbers";
export interface GrainDryingPoint {
timestamp: number;
dryScore: number;
}
interface Props {
data: GrainDryingPoint[];
showStroke?: boolean;
useGradient?: boolean;
displayY?: boolean;
newXDomain?: number[] | string[];
multiGraphZoom?: (domain: number[] | string[]) => void;
multiGraphZoomOut?: boolean;
}
export default function GrainDryingChart(props: Props) {
const { data, showStroke, useGradient, displayY, newXDomain, multiGraphZoom } = props;
const prevData = usePrevious(data);
const theme = useTheme();
const [offset, setOffset] = useState(0);
const now = moment();
const veryDry = red[800];
const dry = orange[500];
const damp = blue[500];
const veryDamp = indigo[800];
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
const [refLeft, setRefLeft] = useState<number | undefined>();
const [refRight, setRefRight] = useState<number | undefined>();
useEffect(() => {
if (newXDomain) {
setXDomain(newXDomain);
}
}, [newXDomain]);
useEffect(() => {
const gradientOffset = () => {
const vpdMax = Math.max(...data.map(p => p.dryScore));
const vpdMin = Math.min(...data.map(p => p.dryScore));
if (vpdMax <= 0) {
return 0;
}
if (vpdMin >= 0) {
return 1;
}
return vpdMax / (vpdMax - vpdMin);
};
if (!prevData || prevData !== data) {
setOffset(gradientOffset());
}
}, [data, prevData, now]);
const zoom = () => {
let newDomain: number[] | string[] = ["dataMin", "dataMax"];
if (refLeft && refRight && refLeft !== refRight) {
refLeft < refRight ? (newDomain = [refLeft, refRight]) : (newDomain = [refRight, refLeft]);
setRefLeft(undefined);
setRefRight(undefined);
if (multiGraphZoom) {
multiGraphZoom(newDomain);
} else {
setXDomain(newDomain);
}
}
};
//just commenting this out as this charts zoom will likely never be controlled from inside
// const zoomOut = () => {
// setXDomain(["dataMin", "dataMax"]);
// };
if (data.length <= 1) return null;
return (
<ResponsiveContainer width={"100%"} height={270}>
<AreaChart
data={data}
onMouseDown={(e: any) => {
if (e) {
setRefLeft(e.activeLabel);
}
}}
onMouseMove={(e: any) => {
if (e) {
setRefRight(e.activeLabel);
}
}}
onMouseUp={() => {
setRefLeft(undefined);
setRefRight(undefined);
zoom();
}}>
{displayY && (
<YAxis
tickFormatter={vpd => {
let val = vpd.toFixed(2);
return val;
}}
type="number"
// label={{
// value: "Water Content",
// angle: -90,
// position: "insideLeft",
// fill: theme.palette.text.primary
// }}
domain={["auto", "auto"]}
tick={{ fill: theme.palette.text.primary }}
/>
)}
<XAxis
allowDataOverflow
dataKey="timestamp"
domain={xDomain}
name="Time"
tickFormatter={timestamp => {
let 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"
/>
<Legend
verticalAlign="top"
payload={[
{ value: "Drying", type: "square", id: "drying", color: orange[800] },
{ value: "Hydrating", type: "square", id: "hydrating", color: blue[800] }
]}
/>
<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={score =>
`${roundTo(parseFloat(String(score)), 2).toString()} ${
score === 0 ? "" : score as number > 0 ? " (Drying)" : " (Hydrating)"
}`
}
/>
)}
/>
<defs>
<linearGradient id="gradientColour" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={veryDry} stopOpacity="90%" />
<stop offset={offset} stopColor={dry} stopOpacity="75%" />
<stop offset={offset} stopColor={damp} stopOpacity="75%" />
<stop offset="100%" stopColor={veryDamp} stopOpacity="90%" />
</linearGradient>
<linearGradient id="solidColour" x1="0" y1="0" x2="0" y2="1">
<stop offset={offset} stopColor={dry} stopOpacity="75%" />
<stop offset={offset} stopColor={damp} stopOpacity="75%" />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey={"dryScore"}
name="Score"
stroke={
showStroke
? useGradient
? "url(#gradientColour)"
: "url(#solidColour)"
: "transparent"
}
strokeWidth={3}
fill={useGradient ? "url(#gradientColour)" : "url(#solidColour)"}
/>
{refLeft && refRight ? (
<ReferenceArea x1={refLeft} x2={refRight} strokeOpacity={0.3} />
) : null}
</AreaChart>
</ResponsiveContainer>
);
}