import { useId, type ComponentProps } from "react" import { cn } from "cn" import { Area, AreaChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts" import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "./chart" import { POINT_AXIS_PADDING, PointColorTooltip, PointDot, PointValueLabel } from "./line-points" import { seriesToConfig, type ChartSeries } from "./series" // Area fill: the series color is strongest at the top of each area and fades towards the bottom. The overlap of two plain areas stays // readable because each fill is translucent; stacked areas are a bit denser because they never overlap. const FILL_TOP = 0.5 const FILL_BOTTOM = 0.05 const STACKED_FILL_TOP = 0.65 const STACKED_FILL_BOTTOM = 0.2 export type HMAreaChartProps = { data: ReadonlyArray> /** Field of each data row used for the category (X) axis. */ categoryKey: string series: readonly ChartSeries[] /** Line interpolation for every series: `monotone` (default), `linear` or `step`. */ curve?: "monotone" | "linear" | "step" /** Small Dot on every point (default off; the active point and tooltip always appear on hover). Ignored when `pointLabels` or `pointColorKey` draw their own Dots. */ dots?: boolean /** Larger Dot and value Label on every point, placed like the Line chart labels (omitted where they would collide). Not for stacked or expanded areas. */ pointLabels?: boolean /** Per-point colors for Dots, Labels and tooltip indicators; same as `HMLineChart`. */ pointColorKey?: string | Readonly> /** Stack the series: each area sits on the previous one, so the top edge is the running total. */ stacked?: boolean /** Stack and normalize each category to 100% (Recharts `stackOffset="expand"`). The Y axis shows percent; the tooltip shows the original value and its share. A total of 0 shows 0%. */ expanded?: boolean /** Accessible name of the chart. */ label: string className?: string } /** Area chart (shadcn Chart + Recharts): vertical gradient fills in HM series colors, plain, stacked or expanded to 100%. */ export function HMAreaChart({ data, categoryKey, series, curve = "monotone", dots = false, pointLabels = false, pointColorKey, stacked = false, expanded = false, label, className, }: HMAreaChartProps) { const scope = `hm-area-${useId().replace(/:/g, "")}` const gradientId = (key: string) => `${scope}-${key}` const stack = stacked || expanded const colorKeys: Readonly> = typeof pointColorKey === "string" ? (series[0] ? { [series[0].key]: pointColorKey } : {}) : (pointColorKey ?? {}) const customPoints = !stack && (pointLabels || Object.keys(colorKeys).length > 0) const seriesKeys = series.map(item => item.key) const share: ComponentProps["formatter"] = (_value, _name, item) => { const row = (item?.payload ?? {}) as Record const key = String(item?.dataKey ?? "") const total = seriesKeys.reduce((sum, k) => sum + (Number(row[k]) || 0), 0) const value = Number(row[key]) || 0 const percent = total > 0 ? Math.round((value / total) * 100) : 0 return (
{series.find(s => s.key === key)?.label ?? key} {value.toLocaleString()} ยท {percent}%
) } return ( []} margin={{ top: 24, right: 12, left: 0, bottom: 0 }} stackOffset={expanded ? "expand" : undefined} > {series.map(item => ( ))} {/* Inner padding keeps the first and last Dot, Label and active point from being clipped at the plot edge. */} `${Math.round(value * 100)}%` : undefined} /> : Object.keys(colorKeys).length > 0 && customPoints ? : } /> } /> {series.map((item, index) => { const color = `var(--color-${item.key})` const colorKey = colorKeys[item.key] return ( : dots ? { r: 3, fill: color, strokeWidth: 0 } : false} activeDot={customPoints ? : { r: 5 }} > {customPoints && pointLabels ? ( } /> ) : null} ) })} ) }