'use client'; import { useMemo } from 'react'; import { fmt } from '@/lib/format'; import { useTime } from '@/lib/time'; import { AXIS_STYLE, TOOLTIP_STYLE, levelMarkArea, type BarSeriesOption, type EChartsOption, type LineSeriesOption } from './echarts'; import { useEChart } from './useEChart'; export interface SeriesLine { name: string; color: string; points: { ts: string; value: number | null }[]; area?: boolean; width?: number; dashed?: boolean; yAxisIndex?: number; type?: 'line' | 'bar'; } /** Generic small multi-line chart (incident series vs global, ASN BGP series, target TTFB, baselines…). */ export function SeriesChart({ lines, height = 220, yMax = 100, yMin = 0, bands = true, y2, markLines, unit = '' }: { lines: SeriesLine[]; height?: number; yMax?: number | 'auto'; yMin?: number; bands?: boolean; y2?: { max?: number | 'auto'; name?: string }; markLines?: { ts: string; label: string }[]; unit?: string }) { const { mode, format } = useTime(); const option = useMemo(() => { if (!lines.length) return null; return { animation: false, grid: { left: 40, right: y2 ? 40 : 12, top: 10, bottom: 26 }, tooltip: { trigger: 'axis', ...TOOLTIP_STYLE, formatter: (params: unknown) => { const arr = params as { seriesName: string; value: [number, number | null]; color: string }[]; if (!arr.length) return ''; return `
${format(arr[0]!.value[0], 'short')}
` + arr.map((p) => `
${p.seriesName} ${p.value[1] == null ? '—' : fmt(p.value[1], Math.abs(p.value[1]) >= 100 ? 0 : 1)}${unit}
`).join(''); }, }, xAxis: { type: 'time', ...AXIS_STYLE, splitLine: { show: false }, axisLabel: { ...AXIS_STYLE.axisLabel, formatter: (v: number) => format(v, 'time').replace(' UTC', '').slice(0, 5) } }, yAxis: [ { type: 'value', min: yMin, max: yMax === 'auto' ? undefined : yMax, ...AXIS_STYLE }, ...(y2 ? [{ type: 'value' as const, min: 0, max: y2.max === 'auto' ? undefined : y2.max, ...AXIS_STYLE, splitLine: { show: false }, name: y2.name, nameTextStyle: { color: '#8B98A5', fontSize: 10 } }] : []), ], series: lines.map((l, i): LineSeriesOption | BarSeriesOption => { const data = l.points.map((p) => [new Date(p.ts).getTime(), p.value]); const markLine: LineSeriesOption['markLine'] = i === 0 && markLines?.length ? { symbol: 'none', silent: true, lineStyle: { color: '#5B6875', type: 'dotted' }, label: { color: '#8B98A5', fontSize: 10, formatter: (p) => String((p as { name?: string }).name ?? ''), position: 'insideEndTop' }, data: markLines.map((m) => ({ xAxis: new Date(m.ts).getTime(), name: m.label })), } : undefined; if (l.type === 'bar') { return { name: l.name, type: 'bar', yAxisIndex: l.yAxisIndex ?? 0, data, itemStyle: { color: l.color, opacity: 0.8 }, barMaxWidth: 6, z: lines.length - i }; } return { name: l.name, type: 'line', yAxisIndex: l.yAxisIndex ?? 0, data, showSymbol: false, lineStyle: { width: l.width ?? 1.25, color: l.color, type: l.dashed ? 'dashed' : 'solid' }, itemStyle: { color: l.color }, areaStyle: l.area ? { color: l.color + '14' } : undefined, connectNulls: false, z: lines.length - i, markArea: i === 0 && bands ? levelMarkArea(0.05) : undefined, markLine, }; }), }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [lines, yMax, yMin, bands, y2, markLines, unit, mode]); const { ref } = useEChart(option); return (
l.name).join(', ')} />
    {lines.map((l) => (
  • ))}
); }