Skip to content

Commit

Permalink
Replace individual storage graphs with combined graph (#13438)
Browse files Browse the repository at this point in the history
* Replace individual storage graphs with combined graph

* replace underscores with spaces

* fix bar height
  • Loading branch information
hawkeye217 authored Aug 30, 2024
1 parent a8dcc87 commit 6a0b5c3
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 17 deletions.
204 changes: 204 additions & 0 deletions web/src/components/graph/CombinedStorageGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { useTheme } from "@/context/theme-provider";
import { generateColors } from "@/utils/colorUtil";
import { useEffect, useMemo } from "react";
import Chart from "react-apexcharts";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getUnitSize } from "@/utils/storageUtil";

type CameraStorage = {
[key: string]: {
bandwidth: number;
usage: number;
usage_percent: number;
};
};

type TotalStorage = {
used: number;
total: number;
};

type CombinedStorageGraphProps = {
graphId: string;
cameraStorage: CameraStorage;
totalStorage: TotalStorage;
};
export function CombinedStorageGraph({
graphId,
cameraStorage,
totalStorage,
}: CombinedStorageGraphProps) {
const { theme, systemTheme } = useTheme();

const entities = Object.keys(cameraStorage);
const colors = generateColors(entities.length);

const series = entities.map((entity, index) => ({
name: entity,
data: [(cameraStorage[entity].usage / totalStorage.total) * 100],
usage: cameraStorage[entity].usage,
bandwidth: cameraStorage[entity].bandwidth,
color: colors[index], // Assign the corresponding color
}));

// Add the unused percentage to the series
series.push({
name: "Unused Free Space",
data: [
((totalStorage.total - totalStorage.used) / totalStorage.total) * 100,
],
usage: totalStorage.total - totalStorage.used,
bandwidth: 0,
color: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5",
});

const options = useMemo(() => {
return {
chart: {
id: graphId,
background: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5",
selection: {
enabled: false,
},
toolbar: {
show: false,
},
zoom: {
enabled: false,
},
stacked: true,
stackType: "100%",
},
grid: {
show: false,
padding: {
bottom: -45,
top: -40,
left: -20,
right: -20,
},
},
legend: {
show: false,
},
dataLabels: {
enabled: false,
},
plotOptions: {
bar: {
horizontal: true,
},
},
states: {
active: {
filter: {
type: "none",
},
},
hover: {
filter: {
type: "none",
},
},
},
tooltip: {
enabled: false,
x: {
show: false,
},
y: {
formatter: function (val, { seriesIndex }) {
if (series[seriesIndex]) {
const usage = series[seriesIndex].usage;
return `${getUnitSize(usage)} (${val.toFixed(2)}%)`;
}
},
},
theme: systemTheme || theme,
},
xaxis: {
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
labels: {
formatter: function (val) {
return val + "%";
},
},
min: 0,
max: 100,
},
yaxis: {
show: false,
min: 0,
max: 100,
},
} as ApexCharts.ApexOptions;
}, [graphId, systemTheme, theme, series]);

useEffect(() => {
ApexCharts.exec(graphId, "updateOptions", options, true, true);
}, [graphId, options]);

return (
<div className="flex w-full flex-col gap-2.5">
<div className="flex w-full items-center justify-between gap-1">
<div className="flex items-center gap-1">
<div className="text-xs text-primary">
{getUnitSize(totalStorage.used)}
</div>
<div className="text-xs text-primary">/</div>
<div className="text-xs text-muted-foreground">
{getUnitSize(totalStorage.total)}
</div>
</div>
</div>
<div className="h-5 overflow-hidden rounded-md">
<Chart type="bar" options={options} series={series} height="100%" />
</div>
<div className="custom-legend">
<Table>
<TableHeader>
<TableRow>
<TableHead>Camera</TableHead>
<TableHead>Storage Used</TableHead>
<TableHead>Percentage of Total Used</TableHead>
<TableHead>Bandwidth</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{series.map((item) => (
<TableRow key={item.name}>
<TableCell className="flex flex-row items-center gap-2 font-medium capitalize">
{" "}
<div
className="size-3 rounded-md"
style={{ backgroundColor: item.color }}
></div>
{item.name.replaceAll("_", " ")}
</TableCell>
<TableCell>{getUnitSize(item.usage)}</TableCell>
<TableCell>{item.data[0].toFixed(2)}%</TableCell>
<TableCell>
{item.name === "Unused Free Space"
? "—"
: `${getUnitSize(item.bandwidth)} / hour`}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}
36 changes: 36 additions & 0 deletions web/src/utils/colorUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Utility function to generate colors based on a predefined palette with slight variations
export const generateColors = (numColors: number) => {
const palette = [
"#008FFB",
"#00E396",
"#FEB019",
"#FF4560",
"#775DD0",
"#3F51B5",
"#03A9F4",
"#4CAF50",
"#F9CE1D",
"#FF9800",
];

const colors = [...palette]; // Start with the predefined palette

for (let i = palette.length; i < numColors; i++) {
const baseColor = palette[i % palette.length];
// Modify the base color slightly by adjusting the brightness for additional colors
const factor = 1 + Math.floor(i / palette.length) * 0.1;
const modifiedColor = adjustColorBrightness(baseColor, factor);
colors.push(modifiedColor);
}

return colors.slice(0, numColors);
};

const adjustColorBrightness = (color: string, factor: number) => {
const rgb = parseInt(color.slice(1), 16);
const r = Math.min(255, Math.floor(((rgb >> 16) & 0xff) * factor));
const g = Math.min(255, Math.floor(((rgb >> 8) & 0xff) * factor));
const b = Math.min(255, Math.floor((rgb & 0xff) * factor));

return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
};
24 changes: 7 additions & 17 deletions web/src/views/system/StorageMetrics.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CombinedStorageGraph } from "@/components/graph/CombinedStorageGraph";
import { StorageGraph } from "@/components/graph/StorageGraph";
import { FrigateStats } from "@/types/stats";
import { getUnitSize } from "@/utils/storageUtil";
import { useMemo } from "react";
import useSWR from "swr";

Expand Down Expand Up @@ -74,22 +74,12 @@ export default function StorageMetrics({
<div className="mt-4 text-sm font-medium text-muted-foreground">
Camera Storage
</div>
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
{Object.keys(cameraStorage).map((camera) => (
<div className="flex-col rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5 flex flex-row items-center justify-between">
<div className="capitalize">{camera.replaceAll("_", " ")}</div>
<div className="text-xs text-muted-foreground">
{getUnitSize(cameraStorage[camera].bandwidth)} / hour
</div>
</div>
<StorageGraph
graphId={`${camera}-storage`}
used={cameraStorage[camera].usage}
total={totalStorage.used}
/>
</div>
))}
<div className="mt-4 bg-background_alt p-2.5 md:rounded-2xl">
<CombinedStorageGraph
graphId={`single-storage`}
cameraStorage={cameraStorage}
totalStorage={totalStorage}
/>
</div>
</div>
);
Expand Down

0 comments on commit 6a0b5c3

Please sign in to comment.