131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
import { useMemo } from "react";
|
|
import type { TrafficPoint } from "../(data)/dashboard-mock-data";
|
|
|
|
type Props = {
|
|
data: TrafficPoint[];
|
|
};
|
|
|
|
const W = 800;
|
|
const H = 240;
|
|
const PAD_L = 46;
|
|
const PAD_R = 12;
|
|
const PAD_T = 12;
|
|
const PAD_B = 28;
|
|
|
|
function formatAxis(value: number) {
|
|
return value >= 1 ? `${value}G` : `${Math.round(value * 1000)}M`;
|
|
}
|
|
|
|
export default function TrafficChart({ data }: Props) {
|
|
const { max, downPath, downArea, upPath, yTicks, xTicks } = useMemo(() => {
|
|
const plotW = W - PAD_L - PAD_R;
|
|
const plotH = H - PAD_T - PAD_B;
|
|
const max = Math.ceil(
|
|
Math.max(...data.flatMap((p) => [p.up, p.down])),
|
|
);
|
|
|
|
const x = (i: number) => PAD_L + (i / (data.length - 1)) * plotW;
|
|
const y = (v: number) => PAD_T + (1 - v / max) * plotH;
|
|
|
|
const toPath = (key: "up" | "down") =>
|
|
data
|
|
.map(
|
|
(p, i) =>
|
|
`${i == 0 ? "M" : "L"}${x(i).toFixed(2)},${y(
|
|
p[key],
|
|
).toFixed(2)}`,
|
|
)
|
|
.join(" ");
|
|
|
|
const downPath = toPath("down");
|
|
const upPath = toPath("up");
|
|
const baseY = (H - PAD_B).toFixed(2);
|
|
const downArea = `${downPath} L${x(data.length - 1).toFixed(
|
|
2,
|
|
)},${baseY} L${PAD_L},${baseY} Z`;
|
|
|
|
const yTicks = Array.from({ length: 5 }, (_, i) => {
|
|
const v = (max / 4) * i;
|
|
return { v, label: formatAxis(v), y: y(v) };
|
|
});
|
|
|
|
const xTicks: { label: string; x: number }[] = [];
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
xTicks.push({ label: data[i]!.label, x: x(i) });
|
|
}
|
|
|
|
return { max, downPath, downArea, upPath, yTicks, xTicks };
|
|
}, [data]);
|
|
|
|
return (
|
|
<svg
|
|
viewBox={`0 0 ${W} ${H}`}
|
|
className="w-full h-auto text-secondary"
|
|
role="img"
|
|
aria-label="Network traffic over the last 24 hours"
|
|
>
|
|
<defs>
|
|
<linearGradient id="traffic-down" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="currentColor" stopOpacity="0.14" />
|
|
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
|
|
</linearGradient>
|
|
<linearGradient id="traffic-up" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#94a3b8" stopOpacity="0.1" />
|
|
<stop offset="100%" stopColor="#94a3b8" stopOpacity="0" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
{yTicks.map((tick, i) => (
|
|
<g key={i}>
|
|
<line
|
|
x1={PAD_L}
|
|
y1={tick.y}
|
|
x2={W - PAD_R}
|
|
y2={tick.y}
|
|
stroke="#94a3b8"
|
|
strokeOpacity="0.18"
|
|
/>
|
|
<text
|
|
x={PAD_L - 8}
|
|
y={tick.y + 3}
|
|
textAnchor="end"
|
|
className="fill-current text-zinc-500 dark:text-zinc-600 text-[10.5px] tabular"
|
|
>
|
|
{tick.label}
|
|
</text>
|
|
</g>
|
|
))}
|
|
|
|
<path d={downArea} fill="url(#traffic-down)" />
|
|
<path
|
|
d={upPath}
|
|
fill="none"
|
|
stroke="#94a3b8"
|
|
strokeWidth="1.25"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
<path
|
|
d={downPath}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.75"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
|
|
{xTicks.map((tick, i) => (
|
|
<text
|
|
key={i}
|
|
x={tick.x}
|
|
y={H - PAD_B + 16}
|
|
textAnchor="middle"
|
|
className="fill-current text-zinc-500 dark:text-zinc-600 text-[10.5px] tabular"
|
|
>
|
|
{tick.label}
|
|
</text>
|
|
))}
|
|
</svg>
|
|
);
|
|
}
|