Files
frigate/web/src/components/graph/SystemGraph.tsx
T

223 lines
5.5 KiB
TypeScript
Raw Normal View History

import { useTheme } from "@/context/theme-provider";
2025-04-22 16:50:21 -05:00
import { useDateLocale } from "@/hooks/use-date-locale";
import { FrigateConfig } from "@/types/frigateConfig";
2026-09-14 08:16:31 -05:00
import { ApexAxisChartSeries, Threshold } from "@/types/graph";
2024-11-04 07:07:57 -07:00
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
2026-03-29 12:58:47 -05:00
import { useCallback, useEffect, useMemo, useRef } from "react";
2026-09-14 08:16:31 -05:00
import ApexCharts from "apexcharts";
import Chart from "react-apexcharts";
2024-04-07 14:36:28 -06:00
import { isMobileOnly } from "react-device-detect";
2025-04-22 16:50:21 -05:00
import { useTranslation } from "react-i18next";
import useSWR from "swr";
2026-03-29 14:03:07 -05:00
import { useTimeFormat } from "@/hooks/use-date-utils";
2024-04-04 10:24:23 -06:00
type ThresholdBarGraphProps = {
graphId: string;
2026-03-24 20:49:05 +08:00
name?: string;
unit: string;
threshold: Threshold;
updateTimes: number[];
data: ApexAxisChartSeries;
2026-03-29 12:58:47 -05:00
isActive?: boolean;
};
2024-04-04 10:24:23 -06:00
export function ThresholdBarGraph({
graphId,
name,
unit,
threshold,
updateTimes,
data,
2026-03-29 12:58:47 -05:00
isActive = true,
2024-04-04 10:24:23 -06:00
}: ThresholdBarGraphProps) {
2026-03-24 20:49:05 +08:00
const displayName = name || data[0]?.name || "";
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
const lastValue = useMemo<number>(
// @ts-expect-error y is valid
() => data[0].data[data[0].data.length - 1]?.y ?? 0,
[data],
);
2024-09-16 18:18:32 -06:00
const yMax = useMemo(() => {
if (unit != "%") {
return undefined;
}
// @ts-expect-error y is valid
const yValues: number[] = data[0].data.map((point) => point?.y);
return Math.max(threshold.warning, ...yValues);
}, [data, threshold, unit]);
const { theme, systemTheme } = useTheme();
2025-04-22 16:50:21 -05:00
const locale = useDateLocale();
const { t } = useTranslation(["common"]);
2026-03-29 14:03:07 -05:00
const timeFormat = useTimeFormat(config);
2025-04-22 16:50:21 -05:00
const format = useMemo(() => {
return t(`time.formattedTimestampHourMinute.${timeFormat}`, {
ns: "common",
});
}, [t, timeFormat]);
2026-05-29 07:53:17 -05:00
const updateTimesRef = useRef(updateTimes);
useEffect(() => {
updateTimesRef.current = updateTimes;
}, [updateTimes]);
const formatTime = useCallback(
(val: unknown) => {
const dateIndex = Math.round(val as number);
let timeOffset = 0;
if (dateIndex < 0) {
2024-11-04 07:07:57 -07:00
timeOffset = 5 * Math.abs(dateIndex);
}
2026-05-29 07:53:17 -05:00
const times = updateTimesRef.current;
const ts = times[Math.max(1, dateIndex) - 1] - timeOffset;
if (isNaN(ts)) {
return "";
}
return formatUnixTimestampToDateTime(ts, {
timezone: config?.ui.timezone,
date_format: format,
locale,
});
},
2026-05-29 07:53:17 -05:00
[config?.ui.timezone, format, locale],
);
const options = useMemo(() => {
return {
chart: {
id: graphId,
selection: {
enabled: false,
},
toolbar: {
show: false,
},
zoom: {
enabled: false,
},
},
colors: [
({ value }: { value: number }) => {
if (value >= threshold.error) {
return "#FA5252";
} else if (value >= threshold.warning) {
return "#FF9966";
} else {
2024-04-16 14:55:24 -06:00
return "#217930";
}
},
],
grid: {
show: false,
},
legend: {
show: false,
},
dataLabels: {
enabled: false,
},
plotOptions: {
bar: {
distributed: true,
},
},
2024-04-18 10:34:18 -06:00
states: {
active: {
filter: {
type: "none",
},
},
},
tooltip: {
theme: systemTheme || theme,
2024-04-14 10:14:10 -06:00
y: {
formatter: (val) => `${val}${unit}`,
},
},
2024-04-04 10:24:23 -06:00
markers: {
size: 0,
},
xaxis: {
tickAmount: isMobileOnly ? 2 : 3,
2024-04-04 10:24:23 -06:00
tickPlacement: "on",
labels: {
rotate: 0,
formatter: formatTime,
2025-06-04 20:48:26 -05:00
style: {
colors: "#6B6B6B",
},
},
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
},
yaxis: {
2024-04-07 14:36:28 -06:00
show: true,
labels: {
formatter: (val: number) => Math.ceil(val).toString(),
2025-06-04 20:48:26 -05:00
style: {
colors: "#6B6B6B",
},
2024-04-07 14:36:28 -06:00
},
min: 0,
2024-09-16 18:18:32 -06:00
max: yMax,
},
2024-04-04 10:24:23 -06:00
} as ApexCharts.ApexOptions;
2024-09-16 18:18:32 -06:00
}, [graphId, threshold, unit, yMax, systemTheme, theme, formatTime]);
useEffect(() => {
ApexCharts.exec(graphId, "updateOptions", options, true, true);
}, [graphId, options]);
const chartData = useMemo(() => {
if (data.length > 0 && data[0].data.length >= 30) {
return data;
}
2026-03-29 12:58:47 -05:00
const dataPointCount = data[0].data.length;
const fakeData = [];
2026-03-29 12:58:47 -05:00
for (let i = dataPointCount; i < 30; i++) {
fakeData.push({ x: i - 30, y: 0 });
}
2026-03-29 12:58:47 -05:00
const paddedFirst = {
...data[0],
data: [...fakeData, ...data[0].data],
};
return [paddedFirst, ...data.slice(1)] as ApexAxisChartSeries;
}, [data]);
2026-03-29 12:58:47 -05:00
const hasBeenActive = useRef(isActive);
useEffect(() => {
if (isActive && hasBeenActive.current === false) {
ApexCharts.exec(graphId, "updateSeries", chartData, true);
}
hasBeenActive.current = isActive;
// only replay animation on visibility change, not data updates
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, graphId]);
return (
2024-05-14 10:06:44 -05:00
<div className="flex w-full flex-col">
<div className="flex items-center gap-1">
2026-03-24 20:49:05 +08:00
<div className="text-xs text-secondary-foreground">{displayName}</div>
<div className="text-xs text-primary">
{lastValue}
{unit}
</div>
</div>
<Chart type="bar" options={options} series={chartData} height="120" />
2024-04-04 14:55:04 -06:00
</div>
);
}