"use client" import type { ReactNode } from "react" import { Bar, BarChart, CartesianGrid, Legend, XAxis, YAxis, Cell } from "recharts" import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig, } from "@/shared/components/ui/chart" import { cn } from "@/shared/lib/utils" /** * 柱状图:统一的 BarChart 配置(CartesianGrid + XAxis + YAxis + ChartTooltip + Bar)。 * * 覆盖以下重复模式: * - GradeDistributionChart(单 Bar + Cell 分桶着色) * - ClassComparisonChart(多 Bar + Legend) * * 默认配置: * - CartesianGrid: vertical=false, strokeDasharray="4 4", strokeOpacity=0.4 * - XAxis: tickLine=false, axisLine=false, tickMargin=8, 默认截断到 8 字符 * - YAxis: tickLine=false, axisLine=false * - Bar: radius=[4,4,0,0] */ export interface BarSeries { /** 数据字段名 */ dataKey: string /** 图例名称 */ name: string /** 颜色(CSS 变量或 hsl 值) */ color: string /** 圆角(默认 [4, 4, 0, 0]) */ radius?: [number, number, number, number] } interface SimpleBarChartProps { /** 图表数据 */ data: Array> /** 柱系列配置(单条或多条) */ bars: BarSeries[] /** X 轴数据字段名 */ xKey: string /** Y 轴定义域(如 [0, 100];不传则不设置 domain) */ yDomain?: [number, number] /** Y 轴是否允许小数(默认 true) */ yAllowDecimals?: boolean /** Y 轴刻度格式化(如百分比) */ yTickFormatter?: (value: number) => string /** X 轴刻度格式化(默认 "default"=截断到 8 字符;设为 null 则不格式化;传函数则自定义) */ xTickFormatter?: ((value: string) => string) | "default" | null /** X 轴截断长度(默认 8) */ xTruncateLength?: number /** Y 轴宽度(默认 36) */ yWidth?: number /** 图表高度类名(默认 "h-[280px]") */ heightClassName?: string /** 图表 margin */ margin?: { left: number; right: number; top: number; bottom: number } /** 是否显示 Legend(多 Bar 时建议 true) */ showLegend?: boolean /** Tooltip 宽度类名(默认 "w-[200px]") */ tooltipClassName?: string /** 自定义 Tooltip formatter(用于自定义 tooltip 内容) */ tooltipFormatter?: (payload: unknown) => ReactNode /** 按数据项着色的映射(key = xKey 值, value = 颜色);用于单 Bar 分桶着色 */ cellColors?: Record /** 自定义 SVG defs(如 patterns、gradients),渲染在 BarChart 内部 */ defs?: ReactNode /** 容器额外类名 */ className?: string } const DEFAULT_X_TRUNCATE_LENGTH = 8 function makeXTickFormatter(truncateLength: number) { return (value: string): string => value.length > truncateLength ? `${value.slice(0, truncateLength)}...` : value } export function SimpleBarChart({ data, bars, xKey, yDomain, yAllowDecimals = true, yTickFormatter, xTickFormatter = "default", xTruncateLength = DEFAULT_X_TRUNCATE_LENGTH, yWidth = 36, heightClassName = "h-[280px]", margin = { left: 8, right: 8, top: 8, bottom: 8 }, showLegend = false, tooltipClassName = "w-[200px]", tooltipFormatter, cellColors, defs, className, }: SimpleBarChartProps) { const chartConfig: ChartConfig = {} for (const b of bars) { chartConfig[b.dataKey] = { label: b.name, color: b.color, } } const resolvedXTickFormatter = xTickFormatter === null ? undefined : xTickFormatter === "default" ? makeXTickFormatter(xTruncateLength) : xTickFormatter const hasCellColors = !!cellColors && bars.length === 1 return ( {defs} ) : ( ) } /> {showLegend ? : null} {bars.map((b) => ( {hasCellColors && cellColors ? data.map((entry) => { const cellKey = String(entry[xKey]) return }) : null} ))} ) }