- new workspace package packages/chart (shadcn/ui Chart + Recharts, HM Theme tokens), examples entry with sample data - Storybook Chart group (src/chart), consumer example (examples/chart), build:chart / build:chart-example scripts - README rewritten as the AI usage guide (install, CSS wiring, chart selection, data rules, verification) - docs: package count/list, Submodule guide (chart workspace, React dedupe), shared AI_GUIDE row, MAINTAINING, story inventory
105 lines
4.6 KiB
TypeScript
105 lines
4.6 KiB
TypeScript
import { cn } from "cn"
|
|
import { CartesianGrid, LabelList, Line, LineChart, 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"
|
|
|
|
export type HMLineChartProps = {
|
|
data: ReadonlyArray<Record<string, string | number>>
|
|
/** Field of each data row used for the category (X) axis. */
|
|
categoryKey: string
|
|
series: readonly ChartSeries[]
|
|
/** Line interpolation for every series. `monotone` (default) is a smooth curve that never overshoots the data; `linear` joins the points with straight segments; `step` runs horizontal then vertical between points. */
|
|
curve?: "monotone" | "linear" | "step"
|
|
/** Show the small default Dot on every point (default). `false` draws only the line; the active point and tooltip still appear on hover. Ignored when `pointLabels` or `pointColorKey` draw their own Dots. */
|
|
dots?: boolean
|
|
/** Draw a larger Dot on every point and its value next to it. With several series the higher value is labelled above its Dot and the lower
|
|
* one below, and labels are thinned on narrow charts (the tooltip always shows every value). */
|
|
pointLabels?: boolean
|
|
/** Per-point colors (CSS colors, e.g. HM palette tokens) for Dots, labels and tooltip indicators; lines and the legend keep the series color.
|
|
* A string names the row field for the first series; an object maps a series key to its row field. Implies the larger Dot. */
|
|
pointColorKey?: string | Readonly<Record<string, string>>
|
|
/** Accessible name of the chart. */
|
|
label: string
|
|
className?: string
|
|
}
|
|
|
|
/** Line chart (shadcn Chart + Recharts) with HM series colors, line-cursor tooltip and legend. */
|
|
export function HMLineChart({
|
|
data,
|
|
categoryKey,
|
|
series,
|
|
curve = "monotone",
|
|
dots = true,
|
|
pointLabels = false,
|
|
pointColorKey,
|
|
label,
|
|
className,
|
|
}: HMLineChartProps) {
|
|
const colorKeys: Readonly<Record<string, string>> =
|
|
typeof pointColorKey === "string" ? (series[0] ? { [series[0].key]: pointColorKey } : {}) : (pointColorKey ?? {})
|
|
const customPoints = pointLabels || Object.keys(colorKeys).length > 0
|
|
const seriesKeys = series.map(item => item.key)
|
|
// A line without Dots keeps the same inner room as the Dot variants, so both align when shown side by side.
|
|
const roomy = customPoints || !dots
|
|
|
|
return (
|
|
<ChartContainer
|
|
config={seriesToConfig(series)}
|
|
role="group"
|
|
aria-label={label}
|
|
className={cn("aspect-auto h-64 w-full sm:h-80", className)}
|
|
>
|
|
<LineChart
|
|
accessibilityLayer
|
|
data={data as Record<string, string | number>[]}
|
|
margin={{ top: roomy ? 24 : 8, right: 12, left: 0, bottom: 0 }}
|
|
>
|
|
<CartesianGrid vertical={false} />
|
|
{/* Inner padding keeps the first and last Dot, label and active point from being clipped at the plot edge. */}
|
|
<XAxis
|
|
dataKey={categoryKey}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickMargin={8}
|
|
padding={roomy ? { left: POINT_AXIS_PADDING, right: POINT_AXIS_PADDING } : undefined}
|
|
/>
|
|
<YAxis tickLine={false} axisLine={false} tickMargin={4} width={40} />
|
|
<ChartTooltip
|
|
cursor
|
|
content={
|
|
Object.keys(colorKeys).length > 0
|
|
? <PointColorTooltip indicator="line" colorKeys={colorKeys} />
|
|
: <ChartTooltipContent indicator="line" />
|
|
}
|
|
/>
|
|
<ChartLegend itemSorter={null} content={<ChartLegendContent />} />
|
|
{series.map((item, index) => {
|
|
const color = `var(--color-${item.key})`
|
|
const colorKey = colorKeys[item.key]
|
|
return (
|
|
<Line
|
|
key={item.key}
|
|
dataKey={item.key}
|
|
type={curve}
|
|
stroke={color}
|
|
strokeWidth={2}
|
|
dot={customPoints ? <PointDot colorKey={colorKey} fallback={color} /> : dots ? { r: 3, fill: color, strokeWidth: 0 } : false}
|
|
activeDot={customPoints ? <PointDot colorKey={colorKey} fallback={color} radius={6} /> : { r: 5 }}
|
|
>
|
|
{pointLabels ? (
|
|
<LabelList
|
|
dataKey={item.key}
|
|
content={
|
|
<PointValueLabel data={data} seriesKeys={seriesKeys} seriesIndex={index} curve={curve} colorKey={colorKey} fallback={color} />
|
|
}
|
|
/>
|
|
) : null}
|
|
</Line>
|
|
)
|
|
})}
|
|
</LineChart>
|
|
</ChartContainer>
|
|
)
|
|
}
|