130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
"use client"
|
|
|
|
const PALETTE = [
|
|
"var(--chart-line-1)",
|
|
"var(--chart-line-2)",
|
|
"var(--chart-line-3)",
|
|
"var(--chart-line-4)",
|
|
"var(--chart-1)",
|
|
"var(--chart-2)",
|
|
"var(--chart-3)",
|
|
"var(--chart-4)",
|
|
]
|
|
|
|
/** Легенда для демо-данных (ключи `msk`, `spb`, …). */
|
|
const LEGACY_LABELS: Record<string, string> = {
|
|
msk: "MSK",
|
|
spb: "SPB",
|
|
fra: "FRA",
|
|
ams: "AMS",
|
|
}
|
|
|
|
interface LatencyChartProps {
|
|
series: Record<string, number[]>
|
|
/** Подписи линий (ключ → короткое имя). Если нет — берётся legacy или сам ключ. */
|
|
labels?: Record<string, string>
|
|
}
|
|
|
|
export function LatencyChart({ series, labels: labelsProp }: LatencyChartProps) {
|
|
const W = 800
|
|
const H = 220
|
|
const pad = { l: 44, r: 12, t: 12, b: 28 }
|
|
const iw = W - pad.l - pad.r
|
|
const ih = H - pad.t - pad.b
|
|
|
|
const entries = Object.entries(series).filter(([, arr]) => arr.length > 0)
|
|
const pointCount = entries.length ? Math.max(...entries.map(([, a]) => a.length), 2) : 60
|
|
|
|
const allVals = entries.flatMap(([, arr]) => arr)
|
|
const maxVal = Math.ceil(Math.max(1, ...allVals) / 20) * 20 + 10
|
|
|
|
const xAt = (i: number, n: number) => pad.l + (i / Math.max(n - 1, 1)) * iw
|
|
const yAt = (v: number) => pad.t + (1 - v / maxVal) * ih
|
|
|
|
const gridLines = [0, 0.25, 0.5, 0.75, 1]
|
|
|
|
const tickIndices = [
|
|
0,
|
|
Math.floor((pointCount - 1) * 0.25),
|
|
Math.floor((pointCount - 1) * 0.5),
|
|
Math.floor((pointCount - 1) * 0.75),
|
|
pointCount - 1,
|
|
]
|
|
|
|
const labelForIndex = (i: number) => {
|
|
const spanMin = 60
|
|
const m = Math.round(spanMin * (1 - i / Math.max(pointCount - 1, 1)))
|
|
return `-${m}м`
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%", height: 220, display: "block" }}>
|
|
{gridLines.map((p, i) => (
|
|
<g key={i}>
|
|
<line
|
|
x1={pad.l}
|
|
x2={W - pad.r}
|
|
y1={pad.t + ih * p}
|
|
y2={pad.t + ih * p}
|
|
style={{ stroke: "var(--border)" }}
|
|
strokeDasharray={p === 1 ? "0" : "2 4"}
|
|
/>
|
|
<text
|
|
x={pad.l - 8}
|
|
y={pad.t + ih * p + 4}
|
|
textAnchor="end"
|
|
fontSize="10"
|
|
style={{ fill: "var(--muted-foreground)" }}
|
|
fontFamily="var(--font-mono, monospace)"
|
|
>
|
|
{Math.round(maxVal * (1 - p))}мс
|
|
</text>
|
|
</g>
|
|
))}
|
|
{tickIndices.map((i) => (
|
|
<text
|
|
key={i}
|
|
x={xAt(i, pointCount)}
|
|
y={H - 6}
|
|
textAnchor="middle"
|
|
fontSize="10"
|
|
style={{ fill: "var(--muted-foreground)" }}
|
|
fontFamily="var(--font-mono, monospace)"
|
|
>
|
|
{labelForIndex(i)}
|
|
</text>
|
|
))}
|
|
{entries.map(([key, arr], idx) => {
|
|
const n = arr.length
|
|
const pts = arr.map((v, i) => `${xAt(i, n).toFixed(1)},${yAt(v).toFixed(1)}`).join(" ")
|
|
const color = PALETTE[idx % PALETTE.length]
|
|
return (
|
|
<polyline
|
|
key={key}
|
|
points={pts}
|
|
fill="none"
|
|
style={{ stroke: color }}
|
|
strokeWidth="1.6"
|
|
strokeLinejoin="round"
|
|
/>
|
|
)
|
|
})}
|
|
</svg>
|
|
<div className="flex flex-wrap gap-x-4 gap-y-2 pt-2 pl-11">
|
|
{entries.map(([key], idx) => {
|
|
const color = PALETTE[idx % PALETTE.length]
|
|
const leg =
|
|
labelsProp?.[key] ?? LEGACY_LABELS[key] ?? key.replace(/^src-/, "")
|
|
return (
|
|
<div key={key} className="flex items-center gap-1.5 text-xs">
|
|
<span className="w-3 h-0.5 rounded" style={{ background: color }} />
|
|
<span className="font-mono text-foreground">{leg}</span>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|