200 lines
6 KiB
TypeScript
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>
|
|
);
|
|
}
|