Skip to content

Chart

Visualize data.
<Chart>
<DonutChart />
<Legend>
<LegendItem />
<LegendItem />
</Legend>
</Chart>
  • Chart: Wraps and provides shared context (colors, currently hovered item, etc.) to child components. Not required if using a display-only chart without user interactions. For BarChart and LineChart, pass categories (the series keys) so the Chart, Legend, and ChartTooltip share one color per series.
  • DonutChart: Displays data as parts of a whole, with an optional center value and label.
  • BarChart: Compares categories, stacks composition, and renders pre-binned histograms.
  • LineChart: Shows one or more series as a trend over time.
  • ChartTooltip: A tokenized hover tooltip used by BarChart and LineChart.
  • Legend: Displays a list of labels and values related to the chart.
import { Chart, DonutChart, Legend, LegendItem } from "stylus-ui/Chart";
const data = [
{ name: "Notion", amount: 281 },
{ name: "VS Code", amount: 142 },
{ name: "Slack", amount: 50 },
];
export default () => (
<Chart className="flex items-center gap-8" data={data} category="name">
<DonutChart value="amount" />
<Legend className="w-48">
{data.map((item) => (
<LegendItem
key={item.name}
dataKey={item.name}
label={item.name}
value={item.amount}
/>
))}
</Legend>
</Chart>
);

For the simplest display, you can use DonutChart standalone, without the Chart wrapper.

import { DonutChart } from "stylus-ui/Chart";
const data = [
{ name: "Notion", amount: 281 },
{ name: "VS Code", amount: 142 },
{ name: "Slack", amount: 50 },
];
export default () => (
<DonutChart category="name" value="amount" data={data} className="size-16" />
);

Each slice takes a color from the accent palette positionally. The available colors are accent-1, accent-2, accent-3, accent-4, accent-5, and accent-neutral (reserved for the "Other" / long-tail bucket).

By default, the chart cycles through the accent series in order. To pin or reorder, pass an array of accent keys to the colors prop:

import { DonutChart } from "stylus-ui/Chart";
const data = [
{ name: "Primary", value: 40 },
{ name: "Secondary", value: 30 },
{ name: "Tertiary", value: 20 },
{ name: "Other", value: 10 },
];
export default () => (
<DonutChart
category="name"
value="value"
data={data}
colors={["accent-1", "accent-3", "accent-5", "accent-neutral"]}
/>
);

Display a sum of all chart segments in the center by setting showValue to true.

import { DonutChart } from "stylus-ui/Chart";
import { formatIntlNumber } from "scribe-web-shared/functions";
const data = [
{ name: "Notion", amount: 2814 },
{ name: "VS Code", amount: 142 },
{ name: "Slack", amount: 503 },
];
export default () => (
<DonutChart category="name" value="amount" data={data} showValue />
);

Long values will automaically shrink to fit.

import { DonutChart } from "stylus-ui/Chart";
import { formatIntlNumber } from "scribe-web-shared/functions";
const data = [
{ name: "Notion", revenue: 4500600 },
{ name: "VS Code", revenue: 3200800 },
{ name: "Slack", revenue: 1800900 },
];
export default () => (
<DonutChart category="name" value="revenue" data={data} showValue />
);

Use the valueFormatter prop to format the display value. For example, you can use the formatIntlNumber helper to display locale-appropriate thousands separators for long numbers.

import { DonutChart } from "stylus-ui/Chart";
import { formatIntlNumber } from "scribe-web-shared/functions";
const data = [
{ name: "Notion", amount: 2814 },
{ name: "VS Code", amount: 142 },
{ name: "Slack", amount: 503 },
];
export default () => (
<DonutChart
category="name"
value="amount"
data={data}
showValue
valueFormatter={(v) => formatIntlNumber(v)}
/>
);

To display text above the value, set label.

import { DonutChart } from "stylus-ui/Chart";
import { formatIntlNumber } from "scribe-web-shared/functions";
const data = [
{ name: "Notion", hours: 281 },
{ name: "VS Code", hours: 142 },
{ name: "Slack", hours: 50 },
];
export default () => (
<DonutChart
category="name"
value="hours"
data={data}
showValue
valueFormatter={(v) => `${formatIntlNumber(v)} hrs`}
label="Yearly average"
/>
);

When there's no data or all values are zero, the chart displays a full ring with a - in the center:

import { DonutChart } from "stylus-ui/Chart";
export default () => (
<DonutChart
category="name"
value="amount"
data={[]}
showValue
label="Yearly average"
/>
);

Combine the DonutChart with the Legend component for interactive data visualization. The Chart wrapper automatically synchronizes hover and click states between components with zero boilerplate:

import React from "react";
import { Chart, DonutChart, Legend, LegendItem } from "stylus-ui/Chart";
const data = [
{ name: "Notion", amount: 281 },
{ name: "VS Code", amount: 142 },
{ name: "GitHub", amount: 87 },
{ name: "Slack", amount: 50 },
{ name: "Figma", amount: 43 },
];
export default () => {
const total = data.reduce((sum, item) => sum + item.amount, 0);
return (
<Chart className="flex items-center gap-8" data={data} category="name">
<DonutChart
value="amount"
label="Yearly average"
valueFormatter={(v) => `${v} hrs`}
showValue
/>
<Legend>
{data.map((item) => (
<LegendItem
key={item.name}
dataKey={item.name}
label={item.name}
value={`${item.amount} hrs (${((item.amount / total) * 100).toFixed(1)}%)`}
/>
))}
</Legend>
</Chart>
);
};

Add click handling to toggle selection by providing an onClick callback to the Chart component:

import React from "react";
import { Chart, DonutChart, Legend, LegendItem } from "stylus-ui/Chart";
const data = [
{ name: "Notion", amount: 281 },
{ name: "VS Code", amount: 142 },
{ name: "GitHub", amount: 87 },
{ name: "Slack", amount: 50 },
{ name: "Figma", amount: 43 },
];
export default () => {
const [selected, setSelected] = React.useState(null);
const total = data.reduce((sum, item) => sum + item.amount, 0);
return (
<Chart
className="flex items-center gap-8"
data={data}
category="name"
selectedDataKey={selected}
onClick={setSelected}
>
<DonutChart
value="amount"
label="Yearly average"
valueFormatter={(v) => `${v} hrs`}
showValue
/>
<Legend>
{data.map((item) => (
<LegendItem
key={item.name}
dataKey={item.name}
label={item.name}
value={`${item.amount} hrs (${((item.amount / total) * 100).toFixed(1)}%)`}
/>
))}
</Legend>
</Chart>
);
};

Each legend item accepts an icon prop for FontAwesome icons or a logo prop (with name and src) to display a favicon.

import React from "react";
import { Chart, DonutChart, Legend, LegendItem } from "stylus-ui/Chart";
import { getFavicon } from "stylus-ui/utils/getFavicon";
import { faGrid2 } from "@fortawesome/pro-regular-svg-icons";
const data = [
{ name: "Notion", url: "https://notion.so", amount: 281 },
{ name: "Slack", url: "https://slack.com", amount: 142 },
{ name: "Figma", url: "https://figma.com", amount: 87 },
{ name: "Others", amount: 50 },
];
export default () => {
const total = data.reduce((sum, item) => sum + item.amount, 0);
return (
<Chart
className="flex items-center gap-8"
data={data}
category="name"
colors={["accent-1", "accent-3", "accent-5", "accent-neutral"]}
>
<DonutChart
value="amount"
label="Yearly average"
valueFormatter={(v) => `${v} hrs`}
showValue
/>
<Legend>
{data.map((item) => {
const logoObj = item.url && {
name: item.name,
src: getFavicon({ url: item.url }),
};
return (
<LegendItem
key={item.name}
dataKey={item.name}
logo={logoObj}
icon={!item.url ? faGrid2 : undefined}
label={item.name}
value={`${((item.amount / total) * 100).toFixed(1)}%`}
/>
);
})}
</Legend>
</Chart>
);
};

A donut chart component for visualizing proportional data with an optional center label. Automatically syncs with Chart context when used inside a Chart component.

value

Required
string

The key in data objects to use as values (e.g., ‘amount’, ‘count’)


category

string

The key in data objects to use as category names (e.g., ‘name’, ‘app’). If not provided, will use category from Chart context.


colors

ChartColorKey[] = "[ 'accent-1', 'accent-2', 'accent-3', 'accent-4', 'accent-5', ]"

Accent color keys assigned positionally to slices (accent-1..accent-5, accent-neutral). Defaults to the accent series.


data

Record<string, any>[]

Array of data objects to display in the chart. Each object should have properties matching the category and value keys. If not provided, will use data from Chart context.


label

string

Optional label text to display above the center value


labelFontSize

number = 15

Optional font size for the label text (in SVG viewBox units). Default: 15


showValue

boolean = false

Whether to hide the center value label


valueFontSize

number

Optional font size for the center value text (in SVG viewBox units).


valueFormatter

(value: number) => string = "(val: number) => val.toString()"

Function to format the center label value. Receives the total sum of all values.


Pass index (the x-axis key) and categories (one or more series keys). Each series becomes a bar, colored positionally from the palette.

import { BarChart } from "stylus-ui/Chart";
const data = [
{ team: "Store Ops", clean: 62, manual: 24, rework: 14 },
{ team: "Distribution", clean: 44, manual: 34, rework: 22 },
{ team: "Merchandising", clean: 58, manual: 30, rework: 12 },
{ team: "Planning", clean: 70, manual: 22, rework: 8 },
];
export default () => (
<div className="h-64 w-full">
<BarChart
data={data}
index="team"
categories={["clean", "manual", "rework"]}
/>
</div>
);

Each series takes an accent color positionally (accent-1, accent-2, …). To pin or reorder, pass colors with accent keys — accent-1, accent-2, accent-3, accent-4, accent-5, or accent-neutral:

<BarChart data={data} index="team" categories={["clean", "manual", "rework"]} colors={["accent-3", "accent-1", "accent-neutral"]} />

Set stacked to combine every series into one bar per x position. Set layout="vertical" for horizontal bars.

import { BarChart } from "stylus-ui/Chart";
const data = [
{ team: "Store Ops", clean: 62, manual: 24, rework: 14 },
{ team: "Distribution", clean: 44, manual: 34, rework: 22 },
{ team: "Planning", clean: 70, manual: 22, rework: 8 },
];
export default () => (
<div className="h-64 w-full">
<BarChart
data={data}
index="team"
categories={["clean", "manual", "rework"]}
stacked
/>
</div>
);

Use variant="histogram" to render pre-binned distribution data as gap-free bars. Compute the bins in your data adapter; the chart renders the buckets it is given.

import { BarChart } from "stylus-ui/Chart";
const bins = [
{ bucket: "0–1m", count: 8 },
{ bucket: "1–2m", count: 22 },
{ bucket: "2–5m", count: 41 },
{ bucket: "5–10m", count: 27 },
{ bucket: "10m+", count: 12 },
];
export default () => (
<div className="h-64 w-full">
<BarChart
data={bins}
index="bucket"
categories={["count"]}
variant="histogram"
/>
</div>
);

A bar chart for categorical comparison, stacked composition, and pre-binned histograms. Colors and hover state sync with Chart context when nested inside a Chart component; otherwise it renders standalone from its own props.

categories

Required
string[]

Series keys to render as bars. Each becomes one <Bar>, colored positionally.


index

Required
string

The key in each data object used for the x-axis (category or histogram bin label).


colors

ChartColorKey[] = "[ 'accent-1', 'accent-2', 'accent-3', 'accent-4', 'accent-5', ]"

Accent color keys assigned positionally to categories (accent-1..accent-5, accent-neutral). Pass an explicit list to pin a color per series. Defaults to the accent series.


data

Record<string, any>[]

Array of data objects, one per x-axis position. Falls back to Chart context data.


desc

string

Accessible description, rendered into the SVG <desc>.


emptyMessage

ReactNode = "No data"

Content shown when there is no data. Default: “No data”.


height

number

Fixed chart height in px. Omit to fill the container responsively.


layout

"horizontal" | "vertical" = "horizontal"

'horizontal' renders vertical bars (x = category); 'vertical' renders horizontal bars (y = category). Default: 'horizontal'.


loading

boolean = false

Render a loading skeleton instead of the chart.


showGrid

boolean = true

Show the cartesian grid. Default: true.


showTooltip

boolean = true

Show the hover tooltip. Default: true.


showXAxis

boolean = true

Show the x-axis. Default: true.


showYAxis

boolean = true

Show the y-axis. Default: true.


stacked

boolean = false

Stack all series into a single bar per x position.


ticks

number[]

Explicit value-axis tick positions. Omit to let recharts choose. Pass this to control the ticks when the raw domain (e.g. seconds) formats to awkward labels.


title

string

Accessible name for the chart, rendered into the SVG <title>. Give charts a title so assistive tech can name the graphic.


valueFormatter

(value: number) => string = "(value: number) => value.toString()"

Formats axis + tooltip values.


variant

"bar" | "histogram" = "bar"

'histogram' renders adjacent, gap-free, square bars for pre-binned distribution data. Binning happens upstream in the data adapter, not here. Default: 'bar'.


width

number

Fixed chart width in px. Omit to fill the container responsively (required for tests/jsdom, which size the container to 0).


Pass index (the x-axis key) and categories (the series keys). Lines interpolate as a monotone curve by default; set curveType="linear" or showDots to change the look.

import { LineChart } from "stylus-ui/Chart";
const data = [
{ week: "W1", runs: 40, edits: 12 },
{ week: "W2", runs: 55, edits: 18 },
{ week: "W3", runs: 48, edits: 22 },
{ week: "W4", runs: 63, edits: 20 },
{ week: "W5", runs: 71, edits: 28 },
];
export default () => (
<div className="h-64 w-full">
<LineChart
data={data}
index="week"
categories={["runs", "edits"]}
showDots
/>
</div>
);

Wrap either chart in Chart with categories so the Legend and hover state stay in sync, exactly like DonutChart.

import { BarChart, Chart, Legend, LegendItem } from "stylus-ui/Chart";
const data = [
{ team: "Store Ops", clean: 62, manual: 24, rework: 14 },
{ team: "Distribution", clean: 44, manual: 34, rework: 22 },
{ team: "Planning", clean: 70, manual: 22, rework: 8 },
];
const categories = ["clean", "manual", "rework"];
export default () => (
<Chart categories={categories} className="flex flex-col gap-4">
<div className="h-56 w-full">
<BarChart data={data} index="team" categories={categories} stacked />
</div>
<Legend className="flex-row flex-wrap gap-2">
<LegendItem dataKey="clean" label="Flows cleanly" value="" />
<LegendItem dataKey="manual" label="Manual handling" value="" />
<LegendItem dataKey="rework" label="Rework" value="" />
</Legend>
</Chart>
);

A line chart for trends and time series with one or more series. Set area to fill under each line with a soft gradient. Colors and hover state sync with Chart context when nested inside a Chart component; otherwise it renders standalone from its own props.

categories

Required
string[]

Series keys to render as lines. Each becomes one line, colored positionally.


index

Required
string

The key in each data object used for the x-axis (e.g. a date or week label).


area

boolean = false

Fill the space under each line with a soft gradient of its accent color, fading to transparent. Default: false.


colors

ChartColorKey[] = "[ 'accent-1', 'accent-2', 'accent-3', 'accent-4', 'accent-5', ]"

Accent color keys assigned positionally to categories (accent-1..accent-5, accent-neutral). Pass an explicit list to pin a color per series. Defaults to the accent series.


connectNulls

boolean = false

Bridge gaps where a series value is null/undefined. Default: false.


curveType

"linear" | "monotone" = "monotone"

Line interpolation. Default: 'monotone'.


data

Record<string, any>[]

Array of data objects, one per x-axis position. Falls back to Chart context data.


desc

string

Accessible description, rendered into the SVG <desc>.


emptyMessage

ReactNode = "No data"

Content shown when there is no data. Default: “No data”.


height

number

Fixed chart height in px. Omit to fill the container responsively.


loading

boolean = false

Render a loading skeleton instead of the chart.


showDots

boolean = false

Draw a dot at each data point. Default: false.


showGrid

boolean = true

Show the cartesian grid. Default: true.


showTooltip

boolean = true

Show the hover tooltip. Default: true.


showXAxis

boolean = true

Show the x-axis. Default: true.


showYAxis

boolean = true

Show the y-axis. Default: true.


ticks

number[]

Explicit value-axis tick positions. Omit to let recharts choose. Pass this to control the ticks when the raw domain (e.g. seconds) formats to awkward labels.


title

string

Accessible name for the chart, rendered into the SVG <title>. Give charts a title so assistive tech can name the graphic.


valueFormatter

(value: number) => string = "(value: number) => value.toString()"

Formats axis + tooltip values.


width

number

Fixed chart width in px. Omit to fill the container responsively (required for tests/jsdom, which size the container to 0).


A tokenized tooltip for cartesian charts. Replaces recharts’ hardcoded white box with a surface that follows the brand-refresh + dark-mode token system. Each series swatch uses the accent fill class the chart passes in colorClassNames, so it matches the rendered bar/line exactly.

active

boolean

Whether the tooltip is active (hovering a data point). Injected by recharts.


colorClassNames

Record<string, string>

Map of series key → accent fill class, so each swatch matches its bar/line.


label

ReactNode

The x-axis label for the hovered point. Injected by recharts.


payload

any[]

The hovered payload entries (one per series). Injected by recharts.


valueFormatter

(value: number) => string = "(value: number) => value.toString()"

Formats each series value shown in the tooltip.


Chart (and its useChartContext hook), Legend, and LegendItem are shared across every chart type.

Chart wrapper component that provides interaction state management and color coordination for child components. Automatically synchronizes hover, click states, and colors between DonutChart, Legend, and other chart components.

children

Required
ReactNode

Child components (DonutChart, Legend, etc.)


categories

string[]

Explicit list of series keys for series charts (BarChart, LineChart). Each key is a dataKey shared across every data point. When provided, colors are assigned positionally to these keys and take precedence over data/category derivation — this is the series model, as opposed to the slice model that data + category expresses for DonutChart.


category

string

The key in data objects to use as category names. Required when data is provided.


colors

ChartColorKey[] | ("orange" | "yellow" | "cyan" | "blue" | "fuchsia" | "brand" | "green" | "teal" | "pink" | "purple" | "red" | "slate")[]

Accent color keys (accent-1..accent-5, accent-neutral) assigned positionally to slices (DonutChart) or series (BarChart/LineChart). Defaults to the accent series.

Legacy hue keys are still accepted for backward compatibility with un-migrated consumers, but they are ignored — the default accent cycle is used instead, so nothing renders a legacy color. Prefer accent keys.


data

Record<string, any>[]

Array of data objects. When provided with category, enables automatic color coordination between child components.


hoveredDataKey

null | string

Controlled hover state (dataKey of the hovered item). When provided, the component operates in controlled mode.


onClick

(dataKey: string | null) => void

Callback fired when an item is clicked. Use this for controlled selection state.


onHover

(dataKey: string | null) => void

Callback fired when an item is hovered. Use this for controlled hover state.


selectedDataKey

null | string

Controlled selection state (dataKey of the selected item). When provided, the component operates in controlled mode.


Hook to access chart interaction context. Returns null if used outside of a Chart provider. Components can use this to sync their interaction state automatically.

A legend component for charts that displays colored indicators with labels and values. Automatically syncs with Chart context when used inside a Chart component.

children

Required
ReactNode

Child LegendItem components to display


A single item in a Legend component. Must be used as a child of Legend. Automatically syncs with Chart context when available.

dataKey

Required
string

Unique dataKey for the legend item. Used for tracking active/highlighted state and syncing with chart data.


label

Required
string

The label text to display (e.g., “Notion”, “Slack”, “Revenue”)


value

Required
string

The value text to display (e.g., “35%”, “1,234”, “$50K”)


icon

IconDefinition

Optional icon to display before the label


labelClassName

string

Optional className to apply to the label text


logo

{ name: string; src: string; }

Optional logo to display before the label


onClick

(dataKey: string | null) => void

Callback fired when clicking the legend item. Receives the item dataKey.


size

"xs" | "sm" = "sm"

Text size for label and value. Default: ‘sm’.