'use client' import * as React from 'react' import { cn } from '@/lib/utils' export type LineChartVariant = 'DEFAULT' | 'ACTIVE' | 'WARNING' | 'CRITICAL' export interface LineChartSeries { id: string label: string data: number[] variant?: LineChartVariant } export interface LineChartProps extends React.HTMLAttributes { series: LineChartSeries[] labels: string[] title?: string height?: number min?: number max?: number showDots?: boolean showArea?: boolean showGrid?: boolean showLegend?: boolean animated?: boolean } const LEFT = 40 const RIGHT = 8 const TOP = 8 const BOTTOM = 24 const SERIES_COLORS = [ 'var(--color-green)', 'var(--color-amber)', 'var(--color-red)', 'var(--color-blue)', ] const VARIANT_COLORS: Record = { DEFAULT: 'var(--text-secondary)', ACTIVE: 'var(--color-green)', WARNING: 'var(--color-amber)', CRITICAL: 'var(--color-red)', } function seriesColor(s: LineChartSeries, idx: number): string { if (s.variant) return VARIANT_COLORS[s.variant] return SERIES_COLORS[idx % SERIES_COLORS.length] } const LineChart = React.forwardRef( ( { className, series, labels, title, height = 160, min, max, showDots = true, showArea = false, showGrid = true, showLegend, animated = true, style, ...props }, ref ) => { const containerRef = React.useRef(null) const [svgW, setSvgW] = React.useState(500) const [drawn, setDrawn] = React.useState(false) React.useEffect(() => { if (!containerRef.current) return const ro = new ResizeObserver(entries => { setSvgW(entries[0].contentRect.width) }) ro.observe(containerRef.current) return () => ro.disconnect() }, []) React.useEffect(() => { const id = requestAnimationFrame(() => setDrawn(true)) return () => cancelAnimationFrame(id) }, []) const allValues = series.flatMap(s => s.data) const dataMin = allValues.length > 0 ? Math.min(...allValues) : 0 const dataMax = allValues.length > 0 ? Math.max(...allValues) : 100 const minV = min ?? dataMin const maxV = max ?? dataMax const range = Math.max(maxV - minV, 1) const plotW = svgW - LEFT - RIGHT const plotH = height - TOP - BOTTOM const n = labels.length function xPos(i: number) { return LEFT + (n <= 1 ? plotW / 2 : (i / (n - 1)) * plotW) } function yPos(v: number) { return TOP + (1 - (v - minV) / range) * plotH } function buildLinePath(data: number[]): string { return data .map((v, i) => `${i === 0 ? 'M' : 'L'} ${xPos(i).toFixed(1)} ${yPos(v).toFixed(1)}`) .join(' ') } function buildAreaPath(data: number[]): string { if (data.length === 0) return '' const line = buildLinePath(data) const firstX = xPos(0).toFixed(1) const lastX = xPos(data.length - 1).toFixed(1) const bottomY = (TOP + plotH).toFixed(1) return `${line} L ${lastX} ${bottomY} L ${firstX} ${bottomY} Z` } const gridFracs = [0, 1 / 3, 2 / 3, 1] const showLeg = showLegend ?? series.length > 1 const step = Math.max(1, Math.ceil(n / 8)) const shouldAnimate = animated return (
{title && (
{title}
)}
{/* Grid */} {showGrid && gridFracs.map((f, gi) => { const y = TOP + f * plotH const val = minV + (1 - f) * range return ( {val.toFixed(0)} ) })} {/* X-axis labels */} {labels.map((label, i) => { if (i % step !== 0 && i !== n - 1) return null return ( {label} ) })} {/* Series */} {series.map((s, si) => { if (s.data.length === 0) return null const color = seriesColor(s, si) const linePath = buildLinePath(s.data) const areaPath = buildAreaPath(s.data) return ( {/* Area fill */} {showArea && ( )} {/* Glow layer */} {/* Main line */} {/* Dots */} {showDots && s.data.map((v, i) => ( ))} ) })}
{/* Legend */} {showLeg && (
{series.map((s, si) => { const color = seriesColor(s, si) return (
{s.label}
) })}
)}
) } ) LineChart.displayName = 'LineChart' export { LineChart }