Files
NextEdu/src/shared/components/charts/simple-bar-chart.tsx
SpecialX c4d3433cc9 feat(shared): add UI components, hooks, form fields, and action utils
- Add UI components: confirm-delete-dialog, empty-table-row, list-pagination, pagination, status-badge

- Add form-fields directory for reusable form field components

- Add hooks: use-action-mutation, use-action-query for server action integration

- Add action-utils lib for action state helpers

- Update a11y components, charts, global-search, onboarding-gate, question components

- Update UI components: chip-nav, filter-bar, page-header, stat-card, stat-item, switch, table

- Update hooks: use-action-with-toast, use-aria-live, use-debounce, use-local-storage, use-media-query, use-permission

- Update lib: a11y, ai, audit-logger, auth-guard, bcrypt-utils, change-logger, download, excel, file-storage, http-utils, login-logger, password-policy, password-security-service, permissions, rate-limit, role-utils, search-params, session, storage-provider

- Update types: action-state, permissions

- Update i18n messages (en, zh-CN) for dashboard, diagnostic, grades, lesson-preparation, settings
2026-06-23 17:38:14 +08:00

167 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<Record<string, string | number>>
/** 柱系列配置(单条或多条) */
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<string, string>
/** 自定义 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 (
<ChartContainer config={chartConfig} className={cn(heightClassName, "w-full", className)}>
<BarChart data={data} margin={margin}>
{defs}
<CartesianGrid vertical={false} strokeDasharray="4 4" strokeOpacity={0.4} />
<XAxis
dataKey={xKey}
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={resolvedXTickFormatter}
/>
<YAxis
domain={yDomain}
allowDecimals={yAllowDecimals}
tickLine={false}
axisLine={false}
tickFormatter={yTickFormatter}
width={yWidth}
/>
<ChartTooltip
content={
tooltipFormatter ? (
<ChartTooltipContent className={tooltipClassName} formatter={tooltipFormatter} />
) : (
<ChartTooltipContent className={tooltipClassName} />
)
}
/>
{showLegend ? <Legend /> : null}
{bars.map((b) => (
<Bar
key={b.dataKey}
dataKey={b.dataKey}
fill={`var(--color-${b.dataKey})`}
radius={b.radius ?? [4, 4, 0, 0]}
>
{hasCellColors && cellColors
? data.map((entry) => {
const cellKey = String(entry[xKey])
return <Cell key={cellKey} fill={cellColors[cellKey]} />
})
: null}
</Bar>
))}
</BarChart>
</ChartContainer>
)
}