Components
Chart
Themed chart components built on Recharts. Includes Line, Bar, and Area variants with consistent styling and custom tooltips.
Import
Charts live behind their own entry, @comitor/ui/chart, because recharts is a heavy optional peer (~100 KB) and most pages never draw a chart. Nothing here is re-exported from the main @comitor/ui barrel.
The entry ships two layers. Ready-made: ComitorLineChart, ComitorBarChart, ComitorAreaChart — axes, grid, tooltip, and palette already chosen. Primitive: the shadcn ChartContainer family, where you assemble recharts yourself.
Name change: the bare names ChartContainer and ChartTooltip belong to the shadcn primitives, so the convenience versions are exported as ComitorChartContainer and ComitorChartTooltip. Older snippets that import the bare names for the Comitor layer will pick up the wrong component.
Line Chart
Use ComitorLineChart for trend data over time.
Bar Chart
Use ComitorBarChart for categorical comparisons. Its grid draws horizontal lines only.
Area Chart
Use ComitorAreaChart for cumulative or volume data. The fill sits at fillOpacity 0.15 so overlapping areas stay readable.
Stacked Bar
Pass stacked to stack multiple series. ComitorAreaChart takes the same prop; ComitorLineChart does not.
Only the top of a stack is rounded. Since 1.5.0 ComitorBarChart gives radius={[4, 4, 0, 0]} to the last series in series — the segment recharts draws on top — and radius={0} to the ones beneath it. Rounding every segment, as it used to, put two rounded corners directly under the next segment, so each junction showed a notch and the column read as a stack of loose pieces rather than one bar. Unstacked bars are unchanged: every bar keeps its two rounded top corners.
Known edge: the rounding is decided per series, not per column. If the last series is 0 in one column, the segment visible at the top of that column belongs to the series below it and draws flat. Fixing it would mean computing the radius column by column, which means giving up <Bar> and drawing the rectangles by hand — a larger price than the fault. Order your series so the one that is never zero sits last.
Legend
showLegend puts a legend under the plot area of all three wrappers. Since 1.5.0 what it mounts is ComitorChartLegend, a token-styled legend cut to the same pattern as ComitorChartTooltip, rather than the bare recharts <Legend />. It is exported on its own too, for charts you assemble yourself.
Why it exists: recharts paints the legend label in the series colour — DefaultLegendContent sets finalLabelStyle.color = … || entry.color. That takes the series' --x, the token that fills a background, and uses it as text, which is exactly the swap the three-role contract forbids: --x fills a background, --x-foreground is text on a solid --x, and --x-ink is that colour on the page background. For a light-toned series the result is not merely pale, it is unreadable — and because the colour moves with the palette there is no single measurement that settles it. The swatch keeps the series colour, which is the honest role for it: it is the background of a small square. The label uses text-foreground, measured by hand on --card in the default palette at 17.4:1, where --muted-foreground would give 3.83:1 — under the 4.5:1 WCAG asks of body text. A legend label is the key to the whole chart, so it is primary text, not secondary.
ComitorChartLegend
| Prop | Type | Default | Description |
|---|---|---|---|
payload | ComitorChartLegendItem[] | — | Supplied by recharts when it renders the legend — never pass it by hand. |
ComitorChartLegendItem
The shape of one entry in the injected payload, exported as a type for anyone writing a legend of their own.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The label. Not the number — this is the field the Tooltip payload calls name. |
color | string | — | The series colour. Paints the swatch, never the text. |
dataKey | string | number | — | The field the series was read from. |
Trap: the legend payload is not shaped like the tooltip payload. In a legend entry the label lives in value; in a tooltip entry value is the number and the label lives in name. Read the wrong one and nothing complains — it compiles, it renders, and every label comes out empty, so the legend is a row of swatches with no words. Only looking at it catches this.
Value formatting
Tooltip values fall back to value.toLocaleString('vi-VN') — the package's default locale. Pass valueFormatter to add units or switch locale. Hover a bar to see it.
Animation
All three wrappers accept isAnimationActive and default it to false — the opposite of the recharts default, deliberately.
Trap: on recharts 2.15.x with React 19.2 the draw-on animation never reaches its last frame, so marks stay frozen on frame one — invisible. You get axes, grid, and legend with no data, and no error or warning. Measured on real DOM: <Bar> renders an empty g.recharts-bar-rectangle, <Line> gets stroke-dasharray="0px 634px", and <Area> is clipped by a rect width="0". Only turn animation back on if your app is pinned to recharts 3.x, where the bug does not exist.
import { ComitorBarChart } from '@comitor/ui/chart'
// Default: no draw-on animation, the chart is painted on first frame.
<ComitorBarChart data={data} xAxisKey="month" series={series} />
// Opt back in — only safe on recharts 3.x.
<ComitorBarChart data={data} xAxisKey="month" series={series} isAnimationActive />Low-level primitives
For chart shapes the three wrappers do not cover, use the shadcn primitives: ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, and the useChart hook. ChartContainer renders a ChartStyle tag that emits a --color-<key> variable per config key, scoped to [data-chart=<id>] — so series colours stay in tokens instead of hex literals in JSX, and a key declaring theme: { light, dark } switches with the theme.
Trap: ChartContainer cannot turn off the animation for you — the marks are children you write. Put isAnimationActive={false} on every <Bar>, <Line>, and <Area> yourself, and write it unconditionally rather than checking your recharts version first. The package's peer range is recharts >=2.15.0, so the version a consuming app resolves is not pinned and is often not a version you know: on 2.15.x the prop is the difference between a chart and an empty grid, and on 3.x it costs nothing, because the prop still exists there and simply means “no draw-on animation”. The container is also aspect-video by default — pass aspect-auto plus a height class for a fixed size.
| Prop | Type | Default | Description |
|---|---|---|---|
configrequired | ChartConfig | — | Per-series label, icon, and colour. Source of the --color-<key> variables. |
childrenrequired | ResponsiveContainer children | — | A single recharts chart element that you assemble yourself. |
id | string | — | Overrides the generated data-chart id that scopes the colour variables. |
className | string | — | Merged onto the wrapper div. The base classes include aspect-video, so pass aspect-auto plus a height class for a fixed height. |
ChartTooltipContent reads labels from config and takes hideLabel, hideIndicator, indicator ('dot' | 'line' | 'dashed'), nameKey, and labelKey. Like the Comitor tooltip it prints numbers with toLocaleString('vi-VN') unless you pass a formatter.
ComitorChartContainer & ComitorChartTooltip
The pieces the three wrappers are built from are exported on their own. ComitorChartContainer is a fixed-height ResponsiveContainer and nothing else — no config context, no --color-<key> variables. ComitorChartTooltip is the token-styled tooltip; hand it to recharts as an element, not a component. The third piece, ComitorChartLegend, is documented under Legend above.
import { ComitorChartContainer, ComitorChartTooltip } from '@comitor/ui/chart'
import { Pie, PieChart, Tooltip } from 'recharts'
// ComitorChartContainer only sizes the ResponsiveContainer — no config,
// no --color-<key> variables. Use it when you want the Comitor tooltip
// on a chart shape the three wrappers do not cover.
export function Example() {
return (
<ComitorChartContainer height={280}>
<PieChart>
<Pie data={data} dataKey="revenue" nameKey="month" isAnimationActive={false} />
<Tooltip content={<ComitorChartTooltip valueFormatter={usd} />} />
</PieChart>
</ComitorChartContainer>
)
}ComitorChartContainer
| Prop | Type | Default | Description |
|---|---|---|---|
height | number | string | 300 | Container height. A number is read as px. |
className | string | — | Merged onto the wrapper div, which is w-full. |
ref | React.Ref<HTMLDivElement> | — | Forwarded to the wrapper div. |
ComitorChartTooltip
| Prop | Type | Default | Description |
|---|---|---|---|
labelFormatter | (label: string) => string | — | Formats the X-axis label at the top of the tooltip. |
valueFormatter | (value: number) => string | — | Formats each value. Falls back to value.toLocaleString('vi-VN'). |
active / payload / label | injected | — | Supplied by recharts when it renders the tooltip — never pass these by hand. |
ComitorChartPayloadItem
The shape of one entry in the tooltip's injected payload, exported as a type so a tooltip of your own can be written against it. Every field is optional — recharts fills what it has.
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | The measured value for this series at the hovered point. |
name | string | — | Series label — the name you gave the series, or its key. |
dataKey | string | number | — | The field this point was read from. |
color | string | — | The series colour, ready for a swatch background. |
payload | Record<string, unknown> & { fill?: string } | — | The source data row this point came from. Added in 1.5.0 — without it a hand-written tooltip cannot get from a point back to its row, so it cannot show any column that is not in series (a project code, an id, a unit). The primitive twin ChartPayloadItem has carried the field all along. |
Color palette
The CHART_COLORS export names the palette in CSS variables. Five of its keys — gold, teal, navy, green, red — point at --chart-1…5 rather than at brand variables, so an app can retheme its charts by overriding those five without touching the brand palette. The two remaining keys are shortcuts to the brand scales instead: goldLight is --gold-100 and ash is --ash-300, so they do not follow a --chart-* override.
goldgoldLighttealnavygreenredashChartColorName, exported from the same entry, is the key type of that object — 'gold' | 'goldLight' | 'teal' | 'navy' | 'green' | 'red' | 'ash'. Reach for it when a tone travels through your own types before it becomes a colour — a config object, a status map, a prop that picks a series colour. Type that field as ChartColorName rather than string and a misspelled tone is a build error, which is worth having because the runtime failure is a quiet one: an unknown key reads back undefined, and a series whose color is undefined does not throw — it takes the next rotation colour, so the chart draws fine and is merely the wrong colour.
import { CHART_COLORS, ComitorBarChart } from '@comitor/ui/chart'
import type { ChartColorName } from '@comitor/ui/chart'
// Name the tone in your own data instead of the CSS variable, and a typo
// is a build error rather than a series that silently falls back to the
// CHART_SERIES_COLORS rotation.
type Metric = { key: string; name: string; tone: ChartColorName }
const METRICS: Metric[] = [
{ key: 'revenue', name: 'Revenue', tone: 'gold' },
{ key: 'users', name: 'Users', tone: 'teal' },
]
export function Example() {
return (
<ComitorBarChart
data={data}
xAxisKey="month"
series={METRICS.map(({ key, name, tone }) => ({ key, name, color: CHART_COLORS[tone] }))}
height={300}
/>
)
}A series with no color falls back to CHART_SERIES_COLORS, rotated by index.
[0] var(--chart-1)[1] var(--chart-2)[2] var(--chart-3)[3] var(--chart-4)[4] var(--chart-5)Both constants live in a plain module with no 'use client' directive, so a Server Component can read their real values. That matters: everything exported through a client module arrives at the server as a client-reference proxy, which would make CHART_COLORS.gold read undefined with no error while TypeScript still reported a literal type. Building a series array in a server page is therefore safe. The key named gray in older copies is now ash.
Props (shared)
Shared by ComitorLineChart, ComitorBarChart, and ComitorAreaChart.
| Prop | Type | Default | Description |
|---|---|---|---|
datarequired | Record<string, unknown>[] | — | Array of data points. |
xAxisKeyrequired | string | — | Name of the field in data used for the X axis. |
seriesrequired | DataSeries[] | — | One entry per line, bar, or area. See the table below. |
height | number | string | 300 | Container height. A number is read as px. |
className | string | — | Passed to the chart container div. |
showGrid | boolean | true | Cartesian grid. Bar charts draw horizontal lines only. |
showLegend | boolean | false | Legend below the plot area. Since 1.5.0 it mounts ComitorChartLegend rather than the bare recharts legend, so the labels are readable text instead of the series colour. See Legend. |
valueFormatter | (value: number) => string | — | Formats tooltip values. Falls back to value.toLocaleString('vi-VN'). |
isAnimationActive | boolean | false | Draw-on animation. Off by default — see Animation below before turning it on. |
stacked | boolean | false | ComitorBarChart and ComitorAreaChart only. Stacks the series instead of grouping them. |
DataSeries
| Prop | Type | Default | Description |
|---|---|---|---|
keyrequired | string | — | Field name in each data row. |
name | string | — | Label shown in the tooltip and legend. Defaults to key. |
color | string | — | Series colour. Defaults to CHART_SERIES_COLORS rotated by index. Use a CHART_COLORS value or var(--chart-N) — never a hex literal. ChartColorName is the type of the key you index CHART_COLORS by. |
Accessibility
- Charts are visual — provide a text summary or data table alternative for screen readers.
- The wrappers do not enable recharts'
accessibilityLayer, so the tooltip follows the pointer and is not reachable by keyboard. Pair any chart that carries real information with a table or a written summary. - Do not rely on hue alone to tell series apart — turn on
showLegend, or give each series a distinctnamethat the tooltip can announce. - Legend labels are ordinary text, not coloured text: the swatch carries the series colour and the label uses
text-foreground, 17.4:1 on--cardin the default palette. A hand-rolled<Legend />gives that contrast away — see Legend above. - The
--chart-*steps are fill colours sized for large areas: on white,--chart-1measures 1.86:1 and--chart-43.13:1. WCAG does not demand 3:1 of a large labelled fill, but it does of thin marks — for 1–2 px strokes and dots prefer the matching ink token (--teal-ink,--green-ink,--red-ink). --chart-3resolves to--navy-inkprecisely so it survives dark mode — raw navy sits at 1.15:1 against the dark background.