import { ReactNode, useState } from 'react'; import { Area, AreaChart, Bar, BarChart, CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts'; import './Chart.css'; export interface Series { key: string; label: string; /** 1-based slot in the categorical palette. Assigned in order, never cycled. */ slot: 1 | 2 | 3 | 4 | 5 | 6; unit?: string; /** Round displayed values to this many decimals. */ decimals?: number; } interface ChartProps { title: string; subtitle?: string; data: Array>; series: Series[]; type: 'line' | 'bar' | 'stacked-bar' | 'area'; xKey?: string; height?: number; /** Y-axis label; a chart has exactly one axis — never two scales. */ unit?: string; footer?: ReactNode; } const fmt = (value: any, decimals = 0) => value == null ? '—' : typeof value === 'number' ? value.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }) : String(value); function TooltipBox({ active, payload, label, series }: any) { if (!active || !payload?.length) return null; return (
{label}
{payload.map((entry: any) => { const s = series.find((x: Series) => x.key === entry.dataKey); return (
); })}
); } function Chart({ title, subtitle, data, series, type, xKey = 'date', height = 240, unit, footer, }: ChartProps) { // A table view is the relief for series whose colour falls below 3:1 on the // light surface, and doubles as the non-visual reading of any chart. const [showTable, setShowTable] = useState(false); const present = series.filter((s) => data.some((row) => row[s.key] != null)); if (present.length === 0) { return (

{title}

暂无数据
); } const color = (s: Series) => `var(--series-${s.slot})`; const axis = { stroke: 'var(--border-strong)', tick: { fill: 'var(--text-muted)', fontSize: 11 }, tickLine: false, }; const margin = { top: 8, right: 8, bottom: 0, left: -8 }; // A legend is mandatory from two series up; a single series is named by the // title, so a legend box would only repeat it. const legend = present.length > 1 ? ( { const s = present.find((x) => x.key === value); return {s?.label ?? value}; }} /> ) : null; const grid = ; const tip = ( } cursor={{ stroke: 'var(--border-strong)', strokeWidth: 1 }} /> ); const render = () => { if (type === 'bar' || type === 'stacked-bar') { const stacked = type === 'stacked-bar'; return ( {grid} {tip} {legend} {present.map((s, i) => ( ))} ); } if (type === 'area') { return ( {present.map((s) => ( ))} {grid} {tip} {legend} {present.map((s) => ( ))} ); } return ( {grid} {tip} {legend} {present.map((s) => ( ))} ); }; return (

{title} {unit && ({unit})}

{subtitle &&

{subtitle}

}
{showTable ? (
{present.map((s) => ( ))} {[...data].reverse().map((row, i) => ( {present.map((s) => ( ))} ))}
日期
{row[xKey]}{fmt(row[s.key], s.decimals)}
) : ( {render()} )} {footer &&
{footer}
}
); } export default Chart;