Charts
The Chart Kit — tiered chart components from in-row micro marks to full plots, with shared states and token-based theming.
Overview
The Chart Kit is the platform's single chart language. It replaces the previous Nivo-based charts with lightweight, token-themed components organized in three tiers:
- Tier 1 — micro marks that live inside a row or a tile:
Delta,Sparkline,MagnitudeBar,HBarChart - Tier 2 — one plot system with a shared axis language:
LineChart,BarChart,TrendChart, framed byChartCard - Tier 3 — specialized forms:
GeoChart,Heatmap,PieChart - Shared states:
ChartEmpty,ChartLoading - Theme tokens:
chartThemeexports — colors are tokens, never hexes
All parts import from @fsai/shared-ui. For how to compose them into a full dashboard — KPI rows, metric switchers, cross-filtering, section states — see the Analytics Display pattern. This page is the API reference.
import {
ChartCard,
LineChart,
BarChart,
TrendChart,
Delta,
Sparkline,
MagnitudeBar,
HBarChart,
GeoChart,
Heatmap,
PieChart,
ChartEmpty,
ChartLoading,
SeriesLegend,
chartAccentColor,
chartSeriesColors,
} from '@fsai/shared-ui';Tier 1: Micro Marks
Micro marks render inside a row, cell, or stat tile. They have no axes, no legend, no card, and no title of their own — the surrounding row supplies the context.
Delta
Change against the previous period.
<Delta value={12.4} />
<Delta value={-3.2} suffix="pp" />
<Delta value={cost.delta} upIsGood={false} />
<Delta value={null} /> {/* renders "New" */}| Prop | Type | Default | Description |
|---|---|---|---|
value | number | null | — | Percent (or suffix-unit) change. null means no prior period — renders "New". |
suffix | string | '%' | Unit appended to the number. |
upIsGood | boolean | true | Direction is colored by intent, not sign — pass false for costs, churn, unsubscribes. |
className | string | — | Additional classes. |
Sparkline
The shape of a series with no scale. Use inside a stat tile; never as the only chart answering a question.
<Sparkline points={dailyImpressions} />| Prop | Type | Default | Description |
|---|---|---|---|
points | number[] | — | The series values, in order. |
width | number | 96 | SVG width in px. |
height | number | 28 | SVG height in px. |
color | string | chartAccentColor | Stroke token. |
Returns null on thin data — fewer than 2 points, or fewer than 3 non-zero values — because a flat line with one spike reads as broken rather than as data. Treat "no sparkline" as a valid outcome; don't reserve space for it.
MagnitudeBar
One value against a known ceiling, inside a table cell or list row. Reads as a proportion, so it carries no axis or ticks.
<MagnitudeBar value={row.followers} max={maxFollowers} />| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | The value to fill. |
max | number | — | The shared ceiling. Non-zero values keep a 2% minimum width so they never vanish. |
color | string | chartAccentColor | Fill token. |
trackColor | string | chartAccentSoftColor | Track token. |
HBarChart / HBarRow
A ranked comparison where the label leads and the bar compares. Beats a pie for more than five categories.
<HBarChart
rows={formats.map((f) => ({
label: f.name,
value: f.rate,
display: `${f.rate.toFixed(1)}%`,
}))}
max={100}
/>HBarChart props:
| Prop | Type | Default | Description |
|---|---|---|---|
rows | HBarChartRow[] | — | { label: string; value: number; display: string } per row. |
max | number | largest row value | Fix the ceiling — e.g. 100 for percentages. |
display is the pre-formatted string; value stays numeric so the bar can scale. Use HBarRow directly (label, value, max, display) when rows need custom placement inside richer list rows.
Tier 2: The Plot System
LineChart and BarChart render a plot and nothing else — no card, no title. ChartCard supplies the frame. All plots share the same axis treatment, tooltip shape, and niceTicks scale so the axis reads the same whether the mark is a line or a bar.
ChartCard
The frame a Tier 2 chart sits in: a title Badge, optional header slots, border and padding.
<ChartCard title="Leads" color="blue" right={<DateRangeControl />}>
<BarChart categories={months} series={series} formatValue={fmtCompact} />
</ChartCard>| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Rendered as a Badge. Omit when supplying headerStart. |
color | ChartTheme | 'blue' | Badge color. Pair it with the chart's hue so the two agree. |
headerStart | ReactNode | — | Replaces the title badge — e.g. to fold a metric switcher into the header. |
right | ReactNode | — | Trailing header slot for controls opposite the title. |
children | ReactNode | — | The plot. |
className | string | — | Additional classes on the card. |
LineChart
<LineChart
series={[
{ key: 'leads', label: 'Leads', values, color: chartAccentColor, area: true },
]}
xLabel={(i) => dayLabels[i]}
formatValue={fmtCompact}
/>| Prop | Type | Default | Description |
|---|---|---|---|
series | ChartSeries[] | — | See series shape below. |
xLabel | (index: number) => string | — | Label for each x slot (axis + tooltip heading). |
formatValue | (value: number) => string | — | Formats axis ticks and tooltip values. |
height | number | 240 | Plot height in px. |
seriesLabelAt | (seriesKey: string, index: number) => string | — | Per-series tooltip label at an index — how TrendChart shows "Nov 3" vs "Oct 27" at the same slot. |
legend | boolean | auto | Defaults on when there is more than one series. Pass false when supplying your own SeriesLegend. |
ariaLabel | string | 'Line chart' | Accessible name for the plot. |
ChartSeries shape:
| Field | Type | Description |
|---|---|---|
key | string | Stable identity. |
label | string | Legend and tooltip label. |
values | number[] | One value per x slot. |
color | string | A chart color token. |
area | boolean | The area wash. Only the primary series gets one — two washes muddy each other. |
muted | boolean | Rendered thinner and behind, for comparison series. |
BarChart
<BarChart
categories={['Jan', 'Feb', 'Mar']}
series={[
{ key: 'new', label: 'New', values: [12, 18, 9] },
{ key: 'returning', label: 'Returning', values: [7, 11, 14] },
]}
stacked
formatValue={fmtCompact}
/>| Prop | Type | Default | Description |
|---|---|---|---|
categories | string[] | — | One entry per x-axis slot. |
series | BarChartSeries[] | — | Like ChartSeries but color is optional — omitted colors walk chartSeriesColors in order (the multi-series rule). |
stacked | boolean | false | Stack segments to a per-category total instead of grouping. |
formatValue | (value: number) => string | — | Formats ticks and tooltip values. |
height | number | 240 | Plot height in px. |
ariaLabel | string | 'Bar chart' | Accessible name. |
TrendChart
Not a chart of its own — LineChart with a comparison series, so every change to the line language lands in both.
<TrendChart
current={vm.trendCur}
previous={vm.trendPrev}
showPrevious={showPrev}
dayLabel={vm.dayLabel}
formatValue={fmtCompact}
/>| Prop | Type | Default | Description |
|---|---|---|---|
current | number[] | — | Current-period series (accent color, area wash). |
previous | number[] | — | Previous-period series (muted, behind). |
showPrevious | boolean | — | Toggle the comparison series. |
dayLabel | (index: number, which: 'current' | 'previous') => string | — | Same index, two periods — what makes the comparison readable. |
formatValue | (value: number) => string | — | Formats ticks and tooltip values. |
height | number | 240 | Plot height in px. |
ariaLabel | string | 'Trend over time, current versus previous period' | Accessible name. |
TrendChart sets legend={false} — the period comparison is the one case where the caller supplies a labeled SeriesLegend of its own, usually beside the compare toggle.
SeriesLegend
<SeriesLegend
series={[
{ label: 'Current period', color: chartAccentColor },
{ label: 'Previous period', color: chartMutedColor },
]}
/>| Prop | Type | Description |
|---|---|---|
series | Array<{ label: string; color: string }> | Legend entries. |
children | ReactNode | Trailing content on the legend row. |
niceTicks(max) is also exported — the 1/2/2.5/5/10-stepped tick scale the plots share — for the rare custom plot that must match the axis language.
Tier 3: Specialized Charts
These answer a different shape of question and are self-framing — they take a title and render their own card and list view. Do not wrap them in ChartCard.
PieChart
Composition of a small set. Use for five or fewer slices; ranked lists beat pies beyond that.
<PieChart
data={sources.map((s) => ({ label: s.name, value: s.count }))}
title="Lead Sources"
valueHeader="Leads"
isLoading={isLoading}
/>| Prop | Type | Default | Description |
|---|---|---|---|
data | Array<{ value: number; label: string }> | — | The slices. |
title | string | — | Card title. |
labelHeader | string | 'Label' | List-view label column header. |
valueHeader | string | 'Count' | List-view value column header. |
modifiers | Array<{ label; isActive; onToggle }> | — | Header toggle chips (e.g. "Include closed"). |
isLoading | boolean | — | Renders the loading state. |
formatValue | (value: number) => string | toLocaleString | Value formatting. |
GeoChart
US or Canada choropleth with a list view.
<GeoChart
data={rows.map((r) => ({ state: r.state, value: r.leads }))}
formatValue={fmtCompact}
title="Leads by State"
valueLabel="Leads"
/>| Prop | Type | Default | Description |
|---|---|---|---|
country | 'us' | 'ca' | 'us' | Which map to draw. Data rows for the other country are simply not on the map. |
data | Array<{ state: State; value: number }> | — | State is the SDK type and spans both countries. |
formatValue | (value: number) => string | number | — | Value formatting. |
title | string | — | Card title. |
valueLabel | string | — | Header for the value column in list view — "Leads", "Locations". |
isLoading | boolean | — | Renders the loading state. |
Heatmap
When, across a week, activity lands.
<Heatmap grid={heatGrid} unit="engagements" />| Prop | Type | Default | Description |
|---|---|---|---|
grid | number[][] | — | number[7][24] — summed per weekday (Sunday-indexed) and hour. Display order is Monday-first; the component reorders. |
unit | string | 'events' | What a cell counts. Plural lowercase noun — feeds the tooltip and cell accessible names. |
dayLabels | string[] | ['Sun', …] | Day names, Sunday-indexed, for locales or shorter labels. |
Cells use chartHeatRamp; zero-value cells use chartEmptyFill. Needs about two full weekly cycles of data to be worth drawing.
States
One spinner, one empty, everywhere — instead of each chart inventing its own hole.
{isLoading ? (
<ChartLoading height={240} />
) : hasData ? (
<LineChart … height={240} />
) : (
<ChartEmpty height={240} />
)}ChartLoading props: height (default 256).
ChartEmpty props:
| Prop | Type | Default |
|---|---|---|
height | number | 256 |
message | string | 'No data for this range' |
hint | string | 'Widen the date range or clear a filter.' |
Match height to the chart being replaced so sections don't jump between states.
Theme Tokens
Chart color is a token, never a hex. All tokens resolve against the platform's CSS variables, which is what makes dark mode work without any JS. Exported from @fsai/shared-ui:
| Export | Use |
|---|---|
ChartTheme (type) | 'blue' | 'green' | 'black' | 'orange' | 'red' — the single-series hues, a subset of BadgeColor so chart and badge colors pair. |
chartMainColorByTheme / chartSoftColorByTheme | Strong hue and its background wash per theme. |
chartAccentColor / chartAccentSoftColor | The default accent (blue) and its wash. |
chartMutedColor | The comparison-series gray. |
chartSeriesColors | Nine ordered multi-series colors — walk in order, never hand-pick, so the same category lands on the same color everywhere. |
chartHeatRamp | Seven monotonic steps for density (heatmap cells, choropleth fills). |
chartEmptyFill | The "no data" cell color, distinct from the ramp's lightest step. |
chartGridColor / chartCrosshairColor / chartSurfaceColor | Frame parts shared by every cartesian chart. |
See Foundations → Colors for how these resolve to semantic color variables.
Guidelines
- Do pick the smallest tier that answers the question — a
Deltain a row beats a chart section - Do frame Tier 2 plots with
ChartCardand pair itscolorwith the chart's hue - Do use
ChartEmpty/ChartLoadingwith aheightmatching the chart they replace - Do let multi-series
BarChartcolors default tochartSeriesColorsorder - Do give plots a meaningful
ariaLabeland pre-formatted values viaformatValue - Don't wrap
PieChartorGeoChartinChartCard— they frame themselves - Don't hard-code hex colors or invent new series palettes — use the theme tokens
- Don't put an area wash on more than one line series
- Don't use a pie beyond five slices — use
HBarChart - Don't build a custom plot when a Tier 2 chart fits — if one is truly needed, keep the axis language via
niceTicksand the theme tokens
For composition — KPI rows, metric switchers, comparison legends, cross-filtering, section-level states — see the Analytics Display pattern.