Data Display · astro-ui · Chart
Chart
Zero-JS SVG charts: bar, line, area, pie, donut, scatter, horizontal bars.
Chart renders pure SVG at build time from a data prop — no chart library, no client JS. Seven types share one API: cartesian charts (bar, line, area, scatter) take labels + series; pie/donut take a single series; hbar renders labeled horizontal progress rows. Colors read –ui-chart-1..6 tokens so charts follow the theme, and a visually hidden data table carries the values for screen readers.
Features
- 26 types: bar, line, area, pie, donut, pie3d, treemap, funnel, rose, histogram, waffle, bullet, dumbbell, scatter, hbar, combo, gauge, radar, radial, sankey, waterfall, heatmap, cohort, terrain, map, boxplot
- Pure build-time SVG — zero JS, zero chart library
- Token-driven colors (--ui-chart-1..6) — themable, dark-mode aware
- Grouped or stacked series (stack prop, bars and hbar part-to-whole), curved lines, gradient fills (area/bar/pie/donut), custom per-series colors, nice y-axis ticks
- Native hover titles per point/slice/bar
- Visually hidden data table for screen readers + figcaption
Usage
---
import { Chart } from "astro-ui";
---
<Chart
type="bar"
title="Revenue by month"
labels={["Jan", "Feb", "Mar"]}
series={[{ name: "Revenue", data: [12, 30, 24] }]}
/>Demos
Bar
<Chart
type="bar"
title="Revenue growth"
description="Monthly revenue, 2026"
labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
series={[{ name: "Revenue", data: [247,255,208,204,176,164] ]}
/>Grouped bars
<Chart
type="bar"
title="Desktop vs mobile"
labels={["Jan", "Feb", "Mar", "Apr"]}
series={[
{ name: "Desktop", data: [245,198,193,248] },
{ name: "Mobile", data: [113,159,194,204] },
]}
/>Stacked bars
<Chart
type="bar"
stack
title="Sessions by source"
description="Stacked per day — axis spans daily totals"
labels={["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
series={[
{ name: "Direct", data: [320, 360, 310, 420, 480, 260, 210] },
{ name: "Search", data: [280, 300, 340, 380, 410, 220, 180] },
{ name: "Social", data: [120, 140, 110, 160, 190, 240, 200] },
]}
/>100% stacked bars
<Chart
type="bar"
stack="percent"
title="Traffic mix"
description="Each column normalized to 100% of its daily total"
labels={["Mon", "Tue", "Wed", "Thu", "Fri"]}
series={[
{ name: "Direct", data: [320, 360, 310, 420, 480] },
{ name: "Search", data: [280, 300, 340, 380, 410] },
{ name: "Social", data: [120, 140, 110, 160, 190] },
]}
/>
<!-- stack: true | "percent" -->Gradient bars
<Chart
type="bar"
gradient
title="Deploys per week"
description="Each bar fades toward the baseline"
labels={["W1", "W2", "W3", "W4", "W5", "W6"]}
series={[{ name: "Deploys", data: [14, 22, 18, 27, 31, 24] }]}
/>
<!-- gradient works on bar, combo bars, pie and donut too -->Pill bars
<Chart
type="bar"
barStyle="pill"
title="Pulse"
description="Thin rounded columns — an equalizer look"
labels={["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20"]}
series={[{ name: "Load", data: [8,14,22,30,38,44,40,32,20,10,16,28,40,46,42,30,18,10,26,44] }]}
legend={false}
/>
<!-- barStyle="pill" renders one thin fully-rounded column per label;
colors follow the series (use one series per color band) -->Combo (bars + line)
| Category | Revenue | Orders |
|---|---|---|
| Oct 1 | 4200 | 148 |
| Oct 2 | 4600 | 158 |
| Oct 3 | 5100 | 172 |
| Oct 4 | 4900 | 165 |
| Oct 5 | 5600 | 184 |
| Oct 6 | 6100 | 196 |
| Oct 7 | 6900 | 210 |
| Oct 8 | 5800 | 186 |
| Oct 9 | 5400 | 178 |
<Chart
type="combo"
title="Orders vs revenue"
description="Bars ride the left axis, the line a secondary right axis"
labels={["Oct 1", "Oct 2", "Oct 3", "Oct 4", "Oct 5", "Oct 6", "Oct 7", "Oct 8", "Oct 9"]}
series={[
{ name: "Revenue", data: [4200, 4600, 5100, 4900, 5600, 6100, 6900, 5800, 5400] },
{ name: "Orders", data: [148, 158, 172, 165, 184, 196, 210, 186, 178], kind: "line", color: "var(--ui-success)" }
]}
/>
<!-- per-series kind: "bar" (default) or "line"; rightMaxY overrides the right-axis ceiling -->Line
| Category | This week | Last week |
|---|---|---|
| Mon | 500 | 610 |
| Tue | 524 | 533 |
| Wed | 586 | 587 |
| Thu | 552 | 592 |
| Fri | 546 | 611 |
| Sat | 539 | 607 |
| Sun | 588 | 655 |
<Chart
type="line"
title="Active users"
labels={["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
series={[
{ name: "This week", data: [500,524,586,552,546,539,588] },
{ name: "Last week", data: [610,533,587,592,611,607,655] },
]}
/>Curved line
| Category | Visitor-line |
|---|---|
| Jan | 247 |
| Feb | 250 |
| Mar | 247 |
| Apr | 209 |
| May | 191 |
| Jun | 260 |
<Chart
type="line"
curve
title="Visitor flow"
description="Monotone cubic smoothing — the curve never overshoots the data"
labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
series={[
{ name: "Visitor-line", data: [247,250,247,209,191,260] },
]}
/>Step line
| Category | Desktop | Mobile |
|---|---|---|
| Jan | 420 | 265 |
| Feb | 615 | 430 |
| Mar | 545 | 350 |
| Apr | 760 | 505 |
| May | 480 | 390 |
| Jun | 825 | 545 |
| Jul | 570 | 415 |
| Aug | 690 | 585 |
| Sep | 515 | 445 |
| Oct | 735 | 615 |
| Nov | 610 | 520 |
| Dec | 455 | 380 |
<Chart
type="line"
curve="step"
title="Desktop vs mobile"
description="Plateaus hold until the midpoint between values, then step"
labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]}
series={[
{ name: "Desktop", data: [420, 615, 545, 760, 480, 825, 570, 690, 515, 735, 610, 455] },
{ name: "Mobile", data: [265, 430, 350, 505, 390, 545, 415, 585, 445, 615, 520, 380] },
]}
/>
<!-- curve accepts true (smooth) or "step"; combine with per-series color freely -->Bump curve
| Category | Desktop | Mobile |
|---|---|---|
| Jan | 320 | 180 |
| Feb | 180 | 320 |
| Mar | 420 | 240 |
| Apr | 260 | 450 |
| May | 510 | 290 |
| Jun | 380 | 460 |
<Chart
type="line"
curve="bump"
title="Rank changes"
description="Flat out of each point, S-curve into the next"
labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
series={[
{ name: "Desktop", data: [320, 180, 420, 260, 510, 380] },
{ name: "Mobile", data: [180, 320, 240, 450, 290, 460] },
]}
/>
<!-- curve: true | "step" | "bump" -->Dashed & dotted
| Category | Sessions |
|---|---|
| Apr 1 | 597 |
| Apr 5 | 488 |
| Apr 10 | 386 |
| Apr 15 | 455 |
| Apr 18 | 480 |
| Category | Actual | Target | Stretch |
|---|---|---|---|
| Jan | 535 | 395 | 380 |
| Feb | 568 | 388 | 345 |
| Mar | 554 | 380 | 337 |
| Apr | 586 | 437 | 369 |
<Chart
type="line"
stroke="dashed"
title="Forecast"
description="Whole series dashed — chart-level stroke, per-series override"
labels={["Apr 1", "Apr 5", "Apr 10", "Apr 15", "Apr 18"]}
series={[{ name: "Sessions", data: [597,488,386,455,480] ]}
/>
<Chart
type="line"
title="Solid vs dashed vs dotted"
labels={["Jan", "Feb", "Mar", "Apr"]}
series={[
{ name: "Actual", data: [535,568,554,586] },
{ name: "Target", data: [395,388,380,437], stroke: "dashed" },
{ name: "Stretch", data: [380,345,337,369], stroke: "dotted" },
]}
/>
<!-- stroke: "solid" | "dashed" | "dotted", at chart or series level -->Line with bottom fade
| Category | NAV |
|---|---|
| Jan | 42 |
| Feb | 45 |
| Mar | 44 |
| Apr | 48 |
| May | 52 |
| Jun | 50 |
| Jul | 55 |
| Aug | 58 |
| Sep | 57 |
| Oct | 62 |
| Nov | 66 |
| Dec | 71 |
<Chart
type="line"
gradient
title="Portfolio value"
description="gradient on a line renders a soft wash under the stroke"
labels={["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}
series={[{ name: "NAV", data: [42,45,44,48,52,50,55,58,57,62,66,71], color: "var(--ui-success)" }]}
legend={false}
/>
<!-- gradient on a line: a gradient wash fills under the stroke and a
blurred mirror of the line adds a glow that hugs the curve -->Minimal (no grid, no legend)
| Category | ms |
|---|---|
| 12am | 49 |
| 6am | 54 |
| 12pm | 54 |
| 6pm | 50 |
<Chart
type="line"
grid={false}
legend={false}
title="Ping"
labels={["12am", "6am", "12pm", "6pm"]}
series={[{ name: "ms", data: [49,54,54,50] ]}
/>Area
| Category | Signups |
|---|---|
| Q1 | 1591 |
| Q2 | 1994 |
| Q3 | 1976 |
| Q4 | 1672 |
<Chart
type="area"
title="Signups"
labels={["Q1", "Q2", "Q3", "Q4"]}
series={[{ name: "Signups", data: [1591,1994,1976,1672] ]}
/>Curved area
| Category | Sessions |
|---|---|
| Jan | 820 |
| Feb | 1210 |
| Mar | 940 |
| Apr | 580 |
| May | 1120 |
| Jun | 990 |
<Chart
type="area"
curve
title="Campaign traffic"
description="Smoothed area — the fill follows the monotone curve"
labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
series={[{ name: "Sessions", data: [820, 1210, 940, 580, 1120, 990] }]}
/>Gradient area
| Category | Requests |
|---|---|
| Mon | 4200 |
| Tue | 5100 |
| Wed | 4800 |
| Thu | 6300 |
| Fri | 7100 |
| Sat | 3900 |
| Sun | 3400 |
<Chart
type="area"
curve
gradient
title="API requests"
description="Series color fades toward the baseline"
labels={["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
series={[{ name: "Requests", data: [4200, 5100, 4800, 6300, 7100, 3900, 3400] }]}
/>
<!-- gradient fades the series color to transparent at the baseline; combine with curve freely -->Gradient area, custom color
| Category | Revenue |
|---|---|
| Jan | 12 |
| Feb | 18 |
| Mar | 15 |
| Apr | 22 |
| May | 19 |
| Jun | 26 |
<Chart
type="area"
curve
gradient
title="Revenue"
description="The series color drives the line, the fill, and the gradient"
labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
series={[{ name: "Revenue", data: [12, 18, 15, 22, 19, 26], color: "var(--ui-success)" }]}
/>
<!-- fill + gradient always follow the series color: pass color per series,
or retheme globally via --ui-chart-1..6 (area fallback is --ui-chart-1) -->Gradient intensity
| Category | Requests |
|---|---|
| Mon | 4200 |
| Tue | 5100 |
| Wed | 4800 |
| Thu | 6300 |
| Fri | 7100 |
| Sat | 3900 |
| Sun | 3400 |
<Chart
type="area"
curve
gradient
gradientIntensity={2}
title="Strong fade"
description="gradientIntensity scales every fade — 1 is the default, 2 is saturated"
labels={["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
series={[{ name: "Requests", data: [4200, 5100, 4800, 6300, 7100, 3900, 3400] }]}
/>
<!-- try 0.5 for a whisper-subtle fill; works on area, bar, pie and donut fades -->Pattern fills (dots & hatch)
| Category | Fund A | Fund B |
|---|---|---|
| W1 | 312 | 268 |
| W2 | 388 | 231 |
| W3 | 340 | 205 |
| W4 | 296 | 214 |
| W5 | 274 | 248 |
| W6 | 246 | 289 |
| W7 | 231 | 331 |
| W8 | 224 | 352 |
<Chart
type="area"
curve="step"
fill="dots"
title="Portfolio swing"
description="Texture fills tinted with each series color"
labels={["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8"]}
series={[
{ name: "Fund A", data: [312, 388, 340, 296, 274, 246, 231, 224] },
{ name: "Fund B", data: [268, 231, 205, 214, 248, 289, 331, 352], fill: "hatch" },
]}
/>
<!-- fill: "solid" | "dots" | "hatch" | "none", at chart or series level -->Actual vs target (hatch + bare line)
| Category | Actual | Target |
|---|---|---|
| Jan | 240 | 122 |
| Feb | 178 | 202 |
| Mar | 262 | 128 |
| Apr | 335 | 390 |
| May | 212 | 305 |
| Jun | 275 | 172 |
<Chart
type="area"
curve="step"
title="Weekly signups"
description="275 this week — +12.2% vs target"
labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
series={[
{ name: "Actual", data: [240, 178, 262, 335, 212, 275], fill: "hatch" },
{ name: "Target", data: [122, 202, 128, 390, 305, 172], fill: "none", stroke: "dashed" },
]}
/>
<!-- fill="none" renders a bare reference line; hatch/dots texture the area -->Pie
| Category | Share |
|---|---|
| Direct | 435 |
| Search | 286 |
| Social | 151 |
| Referral | 96 |
<Chart
type="pie"
title="Traffic sources"
labels={["Direct", "Search", "Social", "Referral"]}
series={[{ name: "Share", data: [435, 286, 151, 96] }]}
/>Donut
| Category | GB |
|---|---|
| Images | 42 |
| Video | 66 |
| Docs | 21 |
| Other | 11 |
<Chart
type="donut"
title="Storage by type"
labels={["Images", "Video", "Docs", "Other"]}
series={[{ name: "GB", data: [42, 66, 21, 11] }]}
legend={false}
/>Rounded donut (padded slices)
| Category | Share |
|---|---|
| Chrome | 38 |
| Safari | 24 |
| Firefox | 16 |
| Edge | 12 |
| Other | 10 |
<Chart
type="donut"
padAngle={4}
sliceRadius={22}
title="Browser share"
description="padAngle gaps the slices, sliceRadius rounds their corners"
labels={["Chrome", "Safari", "Firefox", "Edge", "Other"]}
series={[{ name: "Share", data: [38, 24, 16, 12, 10] }]}
legend={false}
/>
<!-- tune padAngle (degrees) and sliceRadius (px) independently -->Petal pie (rim-rounded slices)
| Category | Votes |
|---|---|
| A | 28 |
| B | 22 |
| C | 18 |
| D | 14 |
| E | 10 |
| F | 8 |
<Chart
type="pie"
padAngle={2}
sliceRadius={18}
sliceCorners="outer"
title="Vote share"
description="Only the rim corners round — the center tip stays sharp"
labels={["A", "B", "C", "D", "E", "F"]}
series={[{ name: "Votes", data: [28, 22, 18, 14, 10, 8] }]}
legend={false}
/>
<!-- sliceCorners: "all" (default, every corner) | "outer" (rim only) -->3D pie (extruded)
| Category | Share |
|---|---|
| Other | 22 |
| Signal OS | 21 |
| Nova Chat | 16 |
| Pulse | 15 |
| Wave | 14 |
| Orbit | 12 |
<Chart
type="pie3d"
title="Share of voice"
description="Extruded slices with shaded walls and outside callouts"
height={320}
labels={["Other", "Signal OS", "Nova Chat", "Pulse", "Wave", "Orbit"]}
series={[{ name: "Share", data: [22, 21, 16, 15, 14, 12] }]}
sliceLabels="label"
/>
<!-- depth sets the wall height in px (default 36); sliceLabels="label"
renders outside "pct% NAME" callouts, "value"/"value-pct" print on
the top face; sliceRadius is ignored so walls meet the top exactly -->Treemap
| Category | Users |
|---|---|
| Other | 22 |
| Nova Chat | 16 |
| Assistly | 14 |
| Signal OS | 21 |
| Atlas AI | 15 |
| Draft Lab | 12 |
<Chart
type="treemap"
title="Top products"
description="Tile area scales with the value — squarified at build time"
height={300}
labels={["Other", "Nova Chat", "Assistly", "Signal OS", "Atlas AI", "Draft Lab"]}
series={[{ name: "Users", data: [22, 16, 14, 21, 15, 12] }]}
/>
<!-- values read from series[0].data like pie; tiles sort largest-first,
zero values render no tile; tileRadius rounds the tile corners -->Funnel
| Category | Candidates |
|---|---|
| Applied | 1200 |
| Screened | 780 |
| Interview | 430 |
| Offer | 180 |
| Hired | 96 |
<Chart
type="funnel"
title="Hiring pipeline"
description="Stages keep input order — widths scale with the value"
labels={["Applied", "Screened", "Interview", "Offer", "Hired"]}
series={[{ name: "Candidates", data: [1200, 780, 430, 180, 96] }]}
/>
<!-- one centered trapezoid per stage; narrow stages keep their tooltip -->Nightingale rose
| Category | Cases |
|---|---|
| Jan | 12 |
| Feb | 19 |
| Mar | 33 |
| Apr | 27 |
| May | 41 |
| Jun | 22 |
<Chart
type="rose"
title="Cases by month"
description="Equal angles, area encodes the value — Nightingale's polar-area manner"
labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
series={[{ name: "Cases", data: [12, 19, 33, 27, 41, 22] }]}
sliceLabels="value"
/>
<!-- roseMode: "area" (default, radius ∝ √value) | "radius" (radius ∝ value) -->Histogram
| Bin | Count |
|---|---|
| 12–28.3 | 6 |
| 28.3–44.7 | 6 |
| 44.7–61 | 6 |
| 61–77.3 | 7 |
| 77.3–93.7 | 3 |
| 93.7–110 | 2 |
<Chart
type="histogram"
title="Response time"
description="Raw samples auto-binned (Sturges) into touching bars"
series={[{ name: "ms", data: [12, 18, 22, 19, 25, 31, 28, 35, 42, 38, 47, 55, 51, 62, 58, 70, 66, 74, 81, 77, 90, 95, 110, 88, 72, 64, 59, 48, 44, 39] }]}
/>
<!-- bins={12} overrides the count; barGap={0} restores touching bars;
pass raw observations, not counts -->Waffle
| Category | Share |
|---|---|
| Alpha | 55 |
| Beta | 30 |
| Gamma | 15 |
<Chart
type="waffle"
title="Market share"
description="10×10 unit grid — each cell is 1% of the total"
height={220}
labels={["Alpha", "Beta", "Gamma"]}
series={[{ name: "Share", data: [55, 30, 15] }]}
/>
<!-- values read from series[0].data like pie; sub-1% shares render no cell -->Bullet graph
| Measure | Ranges | Actual | Target |
|---|---|---|---|
| Revenue | 150 / 225 / 300 | 270 | 250 |
| Profit | 20 / 25 / 30 | 21 | 26 |
| Orders | 400 / 550 / 700 | 640 | — |
<Chart
type="bullet"
title="Q3 performance"
description="Actual vs target against qualitative ranges (Few's bullet graph)"
height={220}
bullets={[
{ label: "Revenue", ranges: [150, 225, 300], measure: 270, target: 250 },
{ label: "Profit", ranges: [20, 25, 30], measure: 21, target: 26 },
{ label: "Orders", ranges: [400, 550, 700], measure: 640 },
]}
/>
<!-- bullets: { label, ranges (ascending ceilings), measure, target?, color? } -->Dumbbell
| Category | Before | After |
|---|---|---|
| Shop | 32 | 51 |
| Blog | 48 | 44 |
| Docs | 27 | 63 |
<Chart
type="dumbbell"
title="Before vs after"
description="Paired dots with a delta connector — one series is a dot plot"
labels={["Shop", "Blog", "Docs"]}
series={[
{ name: "Before", data: [32, 48, 27] },
{ name: "After", data: [51, 44, 63] },
]}
/>
<!-- the axis need not start at zero; hover the connector for the delta -->Slice labels (value & leader lines)
| Category | Users |
|---|---|
| Chrome | 275 |
| Safari | 200 |
| Firefox | 187 |
| Edge | 173 |
| Other | 90 |
| Category | Users |
|---|---|
| Chrome | 275 |
| Safari | 200 |
| Firefox | 187 |
| Edge | 173 |
| Other | 90 |
<Chart
type="donut"
padAngle={4}
sliceRadius={18}
sliceLabels="value"
title="Browser share"
description="Formatted values printed at each slice centroid"
labels={["Chrome", "Safari", "Firefox", "Edge", "Other"]}
series={[{ name: "Users", data: [275, 200, 187, 173, 90] }]}
legend={false}
/>
<Chart
type="pie"
sliceLabels="label"
title="Browser share"
description="Category names outside, connected by elbow leader lines"
labels={["Chrome", "Safari", "Firefox", "Edge", "Other"]}
series={[{ name: "Users", data: [275, 200, 187, 173, 90] }]}
legend={false}
/>
<!-- sliceLabels: "none" | "value" | "label" -->Slice labels, value + percent
| Category | Accounts |
|---|---|
| Free | 412 |
| Pro | 268 |
| Team | 143 |
| Enterprise | 77 |
| Category | GB |
|---|---|
| Images | 42 |
| Video | 66 |
| Docs | 21 |
| Other | 11 |
<Chart
type="donut"
padAngle={3}
sliceRadius={14}
sliceLabels="value-pct"
title="Plan distribution"
description="Value and share together at each slice centroid"
labels={["Free", "Pro", "Team", "Enterprise"]}
series={[{ name: "Accounts", data: [412, 268, 143, 77] }]}
legend={false}
/>
<Chart
type="donut"
sliceLabels="value-pct"
sliceLabelFormat={(v, pct) => pct + "% of " + v.toLocaleString()}
title="Storage by type"
description="sliceLabelFormat takes (value, pct) and returns any string"
labels={["Images", "Video", "Docs", "Other"]}
series={[{ name: "GB", data: [42, 66, 21, 11] }]}
legend={false}
/>
<!-- sliceLabels: "none" | "value" | "value-pct" | "label" -->Auto-collapsed slices
| Category | Sessions |
|---|---|
| / | 4820 |
| /pricing | 2130 |
| /blog | 1640 |
| /docs | 1180 |
| /changelog | 310 |
| /about | 240 |
| /careers | 180 |
<Chart
type="donut"
collapseBelow={8}
sliceLabels="value-pct"
title="Entry pages"
description="Slices under 8% merge into the trailing Other slice"
labels={["/", "/pricing", "/blog", "/docs", "/changelog", "/about", "/careers"]}
series={[{ name: "Sessions", data: [4820, 2130, 1640, 1180, 310, 240, 180] }]}
legend={false}
/>
<!-- collapseBelow: percent threshold (0 = off); merged slice is muted,
hover lists what it contains -->Gradient donut
| Category | GB |
|---|---|
| Images | 42 |
| Video | 66 |
| Docs | 21 |
| Other | 11 |
<Chart
type="donut"
gradient
title="Storage by type"
description="Radial fade — strong at the rim, clear toward the center"
labels={["Images", "Video", "Docs", "Other"]}
series={[{ name: "GB", data: [42, 66, 21, 11] }]}
legend={false}
/>Custom colors
| Category | Plans |
|---|---|
| Free | 640 |
| Pro | 310 |
| Team | 90 |
Slices fall back to --ui-chart-1..6 tokens — override them globally or pass color per series.
<Chart
type="donut"
title="Plan distribution"
labels={["Free", "Pro", "Team"]}
series={[
{ name: "Plans", data: [640, 310, 90], color: "var(--ui-primary)" },
]}
legend
/>
<!-- per-series colors: color prop or --ui-chart-1..6 tokens -->Scatter
| Category | Team A | Team B |
|---|---|---|
| Mon | 5 | 4 |
| Tue | 6 | 3 |
| Wed | 7 | 3 |
| Thu | 9 | 5 |
| Fri | 9 | 7 |
<Chart
type="scatter"
title="Deploys per day"
labels={["Mon", "Tue", "Wed", "Thu", "Fri"]}
series={[
{ name: "Team A", data: [5,6,7,9,9] },
{ name: "Team B", data: [4,3,3,5,7] },
]}
/>Horizontal bars
<Chart
type="hbar"
title="Top pages"
labels={["/docs/get-started", "/components", "/blocks", "/docs/rtl"]}
series={[{ name: "Views", data: [3880,4706,3866,2923] ]}
/>Stacked hbar (part-to-whole)
<Chart
type="hbar"
stack
legend={false}
title="Capital inflows"
labels={["Funds"]}
series={[
{ name: "WaveMark Capital", data: [8372000], color: "var(--ui-success)" },
{ name: "Envato Finances", data: [2962400], color: "var(--ui-warning)" },
{ name: "QBridge Tech", data: [1545600], color: "var(--ui-error)" },
]}
/>
<!-- one full-width track; each series becomes a segment sized by its share of the total -->Radial gauge
| Category | of 8 TB |
|---|---|
| Used | 64 |
<Chart
type="gauge"
title="Storage used"
description="Tick marks fill clockwise with the value"
labels={["Used"]}
series={[{ name: "of 8 TB", data: [64] }]}
maxY={100}
ticks={40}
legend={false}
/>
<!-- value = series[0].data[0], scale = maxY (default 100); ticks clamps 8–80 -->Gauge at half
| Category | of 200 deals |
|---|---|
| Reached | 102 |
<Chart
type="gauge"
title="Monthly goal"
labels={["Reached"]}
series={[{ name: "of 200 deals", data: [102] }]}
maxY={200}
legend={false}
/>Block gauge
| Category | of $80k budget |
|---|---|
| Spent | $44,220 |
<Chart
type="gauge"
gaugeStyle="block"
gaugeRamp={["var(--ui-success)", "var(--ui-info)", "var(--ui-primary)"]}
title="Total expenses"
labels={["Spent"]}
series={[{ name: "of $80k budget", data: [44220] }]}
maxY={80000}
format={(v) => `$${v.toLocaleString("en-US")}`}
ticks={16}
legend={false}
/>
<!-- gaugeStyle="block" renders stadium segments; gaugeRamp interpolates
colors across the arc so blocks show position (start → end) -->Radial rings
| Category | Users |
|---|---|
| Chrome | 275 |
| Safari | 200 |
| Firefox | 187 |
| Edge | 173 |
| Other | 90 |
<Chart
type="radial"
title="Browser share"
description="One concentric ring per category — outer ring first"
labels={["Chrome", "Safari", "Firefox", "Edge", "Other"]}
series={[{ name: "Users", data: [275, 200, 187, 173, 90] }]}
maxY={275}
/>
<!-- values read from series[0].data, like pie; maxY sets the full-ring value -->Radial half (rainbow)
| Category | Users |
|---|---|
| Chrome | 275 |
| Safari | 200 |
| Firefox | 187 |
| Edge | 173 |
| Other | 90 |
<Chart
type="radial"
radialSweep="half"
title="Browser share"
description="Half-circle rings over the top — outer ring first"
labels={["Chrome", "Safari", "Firefox", "Edge", "Other"]}
series={[{ name: "Users", data: [275, 200, 187, 173, 90] }]}
maxY={275}
legend={false}
/>
<!-- radialSweep: "full" | "half" -->Radar
| Category | Desktop | Mobile |
|---|---|---|
| JavaScript | 78 | 52 |
| Python | 85 | 58 |
| CSS | 90 | 66 |
| Node.js | 62 | 54 |
| React | 74 | 41 |
| TypeScript | 96 | 60 |
<Chart
type="radar"
title="Skill coverage"
description="One axis per label, filled polygons per series"
labels={["JavaScript", "Python", "CSS", "Node.js", "React", "TypeScript"]}
series={[
{ name: "Desktop", data: [78, 85, 90, 62, 74, 96] },
{ name: "Mobile", data: [52, 58, 66, 54, 41, 60] },
]}
/>
<!-- values scale against maxY (default: global data max); one axis per label -->Sankey flow
| Category |
|---|
<Chart
type="sankey"
title="Traffic funnel"
description="Flow value scales each ribbon and node"
nodes={[
{ name: "Organic Search", color: "var(--ui-success)" },
{ name: "Paid Ads", color: "var(--ui-error)" },
{ name: "Social Media", color: "var(--ui-chart-5, var(--ui-warning))" },
{ name: "Landing Page", color: "var(--ui-info)" },
{ name: "Product Page", color: "var(--ui-chart-2, var(--ui-info))" },
{ name: "Cart", color: "var(--ui-warning)" },
{ name: "Bounced", color: "var(--ui-error)" },
{ name: "Purchase", color: "var(--ui-success)" },
]}
links={[
{ source: "Organic Search", target: "Landing Page", value: 42000 },
{ source: "Paid Ads", target: "Landing Page", value: 28000 },
{ source: "Social Media", target: "Landing Page", value: 18000 },
{ source: "Landing Page", target: "Product Page", value: 52000 },
{ source: "Landing Page", target: "Bounced", value: 36000 },
{ source: "Product Page", target: "Cart", value: 31000 },
{ source: "Product Page", target: "Bounced", value: 21000 },
{ source: "Cart", target: "Purchase", value: 24000 },
{ source: "Cart", target: "Bounced", value: 7000 },
]}
/>
<!-- links reference nodes by name; node value = max(in, out) flow -->Waterfall
| Category | Cash |
|---|---|
| Start | $120k |
| Q1 | $45k |
| Q2 | $-25k |
| Q3 | $65k |
| Q4 | $-20k |
| End | $0k |
<Chart
type="waterfall"
title="Cash flow, 2026"
description="Signed deltas stack on a running total; totals anchor to zero"
labels={["Start", "Q1", "Q2", "Q3", "Q4", "End"]}
series={[{ name: "Cash", data: [120, 45, -30, 65, -20, 0] }]}
waterfallTotals={[0, 5]}
format={(v) => `$${v}k`}
/>
<!-- waterfallTotals lists label indices that render as full total bars
and reset the running sum; increases/decreases color automatically -->Heatmap
| Category | Mon | Wed | Fri | Sat |
|---|---|---|---|---|
| -5w | 4 | 7 | 12 | 2 |
| -4w | 9 | 5 | 14 | 1 |
| -3w | 6 | 10 | 9 | 3 |
| -2w | 12 | 8 | 15 | 0 |
| -1w | 8 | 13 | 11 | 2 |
| This week | 11 | 9 | 16 | 5 |
<Chart
type="heatmap"
title="Commit activity"
description="Rows are days, columns are week offsets — darker means more commits"
height={220}
labels={["-5w", "-4w", "-3w", "-2w", "-1w", "This week"]}
series={[
{ name: "Mon", data: [4, 9, 6, 12, 8, 11] },
{ name: "Wed", data: [7, 5, 10, 8, 13, 9] },
{ name: "Fri", data: [12, 14, 9, 15, 11, 16] },
{ name: "Sat", data: [2, 1, 3, 0, 2, 5] },
]}
heatmapRamp={["var(--ui-muted)", "var(--ui-chart-2, var(--ui-info))", "var(--ui-primary)"]}
heatmapLabels
/>
<!-- rows come from series names, columns from labels; heatmapRamp
interpolates from data min to max; heatmapLabels prints values -->Cohort retention
| Category | Sep 1 | Sep 8 | Sep 15 | Sep 22 |
|---|---|---|---|---|
| Week 0 | 1 | 1 | 1 | 1 |
| Week 1 | 0.4 | 0.4 | 0.4 | 0.5 |
| Week 2 | 0.3 | 0.3 | 0.4 | 0.4 |
| Week 3 | 0.3 | 0.3 | 0.3 | 0 |
| Week 4 | 0.3 | 0.3 | 0 | 0 |
<Chart
type="cohort"
title="Weekly signup retention"
description="Rows are signup cohorts, columns are weeks since signup — the empty tail hasn't been observed yet"
height={230}
labels={["Week 0", "Week 1", "Week 2", "Week 3", "Week 4"]}
series={[
{ name: "Sep 1", data: [1, 0.41, 0.33, 0.29, 0.27] },
{ name: "Sep 8", data: [1, 0.39, 0.31, 0.28, 0.26] },
{ name: "Sep 15", data: [1, 0.44, 0.36, 0.33] },
{ name: "Sep 22", data: [1, 0.47, 0.4] },
]}
cohortSizes={[480, 510, 465, 530]}
cohortRamp={["var(--ui-muted)", "var(--ui-chart-2, var(--ui-info))", "var(--ui-primary)"]}
/>
<!-- cohortSizes gives each row its size sublabel; missing tail values
render empty (not-yet-observed); values ≤ 1 are fractions -->Map (choropleth)
| Category |
|---|
<Chart
type="map"
title="Regional adoption"
description="Adoption intensity across operating regions"
height={320}
mapColumns={3}
mapRegions={[
{ name: "Northwest", at: [0, 0], value: 55 },
{ name: "North Central", at: [1, 0], value: 96 },
{ name: "Southwest", at: [0, 1], value: 8 },
{ name: "South Central", at: [1, 1], value: 82 },
{ name: "East", points: "470 0 565 0 640 100 592 292 522 320 470 320", value: 45 },
]}
mapRamp={["var(--ui-muted)", "var(--ui-chart-5, var(--ui-error))"]}
/>Map (country outline)
| Category |
|---|
<Chart
type="map"
title="Revenue by state"
description="Neighboring polygons share boundary vertices, so regions touch with no gaps"
height={400}
mapRegions={countryRegions}
mapRamp={["var(--ui-muted)", "var(--ui-chart-5, var(--ui-error))"]}
zoomable
/>
<!-- regions are freeform polygons; build them from a shared vertex
table (each neighbor references the same vertices) so states are
adjacent and share boundaries exactly. zoomable adds +/−/Reset
buttons, wheel-to-zoom and drag-to-pan (ships a small script) -->Terrain
| Category | d0 | d1 | d2 | d3 | d4 | d5 | d6 | d7 | d8 | d9 | d10 | d11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| -12 | 5m | 7m | 7m | 10m | 10m | 10m | 9m | 9m | 7m | 5m | 5m | 7m |
| -11 | 6m | 7m | 9m | 10m | 13m | 14m | 13m | 11m | 9m | 7m | 6m | 5m |
| -10 | 8m | 8m | 9m | 14m | 14m | 17m | 14m | 12m | 9m | 7m | 6m | 7m |
| -9 | 6m | 9m | 12m | 16m | 20m | 19m | 18m | 16m | 12m | 9m | 5m | 6m |
| -8 | 8m | 8m | 13m | 18m | 24m | 27m | 22m | 17m | 13m | 9m | 7m | 6m |
| -7 | 8m | 9m | 16m | 22m | 30m | 30m | 28m | 23m | 16m | 11m | 7m | 5m |
| -6 | 9m | 10m | 19m | 25m | 34m | 36m | 32m | 26m | 18m | 10m | 6m | 5m |
| -5 | 7m | 14m | 19m | 31m | 39m | 43m | 39m | 28m | 20m | 12m | 7m | 6m |
| -4 | 8m | 13m | 24m | 33m | 44m | 45m | 41m | 31m | 22m | 13m | 8m | 6m |
| -3 | 11m | 16m | 24m | 35m | 47m | 49m | 44m | 33m | 21m | 13m | 7m | 5m |
| -2 | 9m | 16m | 26m | 37m | 47m | 53m | 48m | 36m | 22m | 15m | 10m | 6m |
| -1 | 12m | 16m | 25m | 37m | 47m | 53m | 48m | 37m | 23m | 15m | 9m | 8m |
| 0 | 12m | 15m | 26m | 35m | 48m | 49m | 45m | 35m | 24m | 14m | 9m | 5m |
| 1 | 14m | 16m | 24m | 34m | 44m | 47m | 44m | 32m | 23m | 14m | 10m | 6m |
| 2 | 16m | 17m | 24m | 32m | 42m | 44m | 38m | 29m | 20m | 13m | 10m | 5m |
| 3 | 16m | 18m | 25m | 30m | 38m | 39m | 34m | 27m | 18m | 14m | 9m | 7m |
| 4 | 19m | 21m | 25m | 29m | 32m | 34m | 32m | 26m | 17m | 11m | 8m | 7m |
| 5 | 20m | 21m | 22m | 27m | 31m | 30m | 27m | 22m | 15m | 12m | 8m | 7m |
| 6 | 20m | 21m | 23m | 24m | 28m | 26m | 23m | 19m | 16m | 10m | 8m | 6m |
| 7 | 22m | 21m | 22m | 24m | 24m | 23m | 20m | 18m | 13m | 10m | 7m | 4m |
| 8 | 22m | 23m | 22m | 23m | 21m | 20m | 19m | 14m | 12m | 10m | 7m | 6m |
| 9 | 21m | 20m | 19m | 20m | 17m | 19m | 15m | 14m | 12m | 9m | 7m | 5m |
| 10 | 19m | 18m | 16m | 17m | 16m | 17m | 14m | 13m | 10m | 9m | 6m | 5m |
| 11 | 18m | 17m | 15m | 14m | 14m | 13m | 12m | 10m | 9m | 7m | 5m | 7m |
<Chart
type="terrain"
title="Signal terrain"
description="Peaks and valleys across a sampled field"
height={400}
labels={Array.from({ length: 24 }, (_, i) => String(i - 12))}
series={[
{ name: "d0", color: "var(--ui-error)", data: [5,6,8,6,8,8,9,7,8,11,9,12,12,14,16,16,19,20,20,22,22,21,19,18] },
{ name: "d1", color: "var(--ui-error)", data: [7,7,8,9,8,9,10,14,13,16,16,16,15,16,17,18,21,21,21,21,23,20,18,17] },
{ name: "d2", color: "var(--ui-error)", data: [7,9,9,12,13,16,19,19,24,24,26,25,26,24,24,25,25,22,23,22,22,19,16,15] },
{ name: "d3", color: "var(--ui-error)", data: [10,10,14,16,18,22,25,31,33,35,37,37,35,34,32,30,29,27,24,24,23,20,17,14] },
{ name: "d4", color: "var(--ui-error)", data: [10,13,14,20,24,30,34,39,44,47,47,47,48,44,42,38,32,31,28,24,21,17,16,14] },
{ name: "d5", color: "var(--ui-error)", data: [10,14,17,19,27,30,36,43,45,49,53,53,49,47,44,39,34,30,26,23,20,19,17,13] },
{ name: "d6", color: "var(--ui-error)", data: [9,13,14,18,22,28,32,39,41,44,48,48,45,44,38,34,32,27,23,20,19,15,14,12] },
{ name: "d7", color: "var(--ui-error)", data: [9,11,12,16,17,23,26,28,31,33,36,37,35,32,29,27,26,22,19,18,14,14,13,10] },
{ name: "d8", color: "var(--ui-error)", data: [7,9,9,12,13,16,18,20,22,21,22,23,24,23,20,18,17,15,16,13,12,12,10,9] },
{ name: "d9", color: "var(--ui-error)", data: [5,7,7,9,9,11,10,12,13,13,15,15,14,14,13,14,11,12,10,10,10,9,9,7] },
{ name: "d10", color: "var(--ui-error)", data: [5,6,6,5,7,7,6,7,8,7,10,9,9,10,10,9,8,8,8,7,7,7,6,5] },
{ name: "d11", color: "var(--ui-error)", data: [7,5,7,6,6,5,5,6,6,5,6,8,5,6,5,7,7,7,6,4,6,5,5,7] },
]}
terrainAxes={["X: Position", "Y: Elevation", "Z: Depth"]}
format={(v) => `${v}m`}
/>
<!-- rows are depth (front to back), columns are position, values are
elevation; terrainMode="wire" | "dots" | "both" (default); the
highest/lowest points get black rings + callouts (rename them via
terrainCalloutLabels, hide via terrainCallouts={false}), a fixed
sample lattice gets accent rings, back rows fade and dash -->Terrain (bare)
| Category | d0 | d1 | d2 | d3 | d4 | d5 | d6 | d7 | d8 | d9 | d10 | d11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| -12 | 5m | 7m | 7m | 10m | 10m | 10m | 9m | 9m | 7m | 5m | 5m | 7m |
| -11 | 6m | 7m | 9m | 10m | 13m | 14m | 13m | 11m | 9m | 7m | 6m | 5m |
| -10 | 8m | 8m | 9m | 14m | 14m | 17m | 14m | 12m | 9m | 7m | 6m | 7m |
| -9 | 6m | 9m | 12m | 16m | 20m | 19m | 18m | 16m | 12m | 9m | 5m | 6m |
| -8 | 8m | 8m | 13m | 18m | 24m | 27m | 22m | 17m | 13m | 9m | 7m | 6m |
| -7 | 8m | 9m | 16m | 22m | 30m | 30m | 28m | 23m | 16m | 11m | 7m | 5m |
| -6 | 9m | 10m | 19m | 25m | 34m | 36m | 32m | 26m | 18m | 10m | 6m | 5m |
| -5 | 7m | 14m | 19m | 31m | 39m | 43m | 39m | 28m | 20m | 12m | 7m | 6m |
| -4 | 8m | 13m | 24m | 33m | 44m | 45m | 41m | 31m | 22m | 13m | 8m | 6m |
| -3 | 11m | 16m | 24m | 35m | 47m | 49m | 44m | 33m | 21m | 13m | 7m | 5m |
| -2 | 9m | 16m | 26m | 37m | 47m | 53m | 48m | 36m | 22m | 15m | 10m | 6m |
| -1 | 12m | 16m | 25m | 37m | 47m | 53m | 48m | 37m | 23m | 15m | 9m | 8m |
| 0 | 12m | 15m | 26m | 35m | 48m | 49m | 45m | 35m | 24m | 14m | 9m | 5m |
| 1 | 14m | 16m | 24m | 34m | 44m | 47m | 44m | 32m | 23m | 14m | 10m | 6m |
| 2 | 16m | 17m | 24m | 32m | 42m | 44m | 38m | 29m | 20m | 13m | 10m | 5m |
| 3 | 16m | 18m | 25m | 30m | 38m | 39m | 34m | 27m | 18m | 14m | 9m | 7m |
| 4 | 19m | 21m | 25m | 29m | 32m | 34m | 32m | 26m | 17m | 11m | 8m | 7m |
| 5 | 20m | 21m | 22m | 27m | 31m | 30m | 27m | 22m | 15m | 12m | 8m | 7m |
| 6 | 20m | 21m | 23m | 24m | 28m | 26m | 23m | 19m | 16m | 10m | 8m | 6m |
| 7 | 22m | 21m | 22m | 24m | 24m | 23m | 20m | 18m | 13m | 10m | 7m | 4m |
| 8 | 22m | 23m | 22m | 23m | 21m | 20m | 19m | 14m | 12m | 10m | 7m | 6m |
| 9 | 21m | 20m | 19m | 20m | 17m | 19m | 15m | 14m | 12m | 9m | 7m | 5m |
| 10 | 19m | 18m | 16m | 17m | 16m | 17m | 14m | 13m | 10m | 9m | 6m | 5m |
| 11 | 18m | 17m | 15m | 14m | 14m | 13m | 12m | 10m | 9m | 7m | 5m | 7m |
<Chart
type="terrain"
title="Signal terrain, bare"
description="Dots banded by elevation — primary low, warning mid, error high"
height={320}
labels={Array.from({ length: 24 }, (_, i) => String(i - 12))}
series={[
{ name: "d0", color: "var(--ui-error)", data: [5,6,8,6,8,8,9,7,8,11,9,12,12,14,16,16,19,20,20,22,22,21,19,18] },
{ name: "d1", color: "var(--ui-error)", data: [7,7,8,9,8,9,10,14,13,16,16,16,15,16,17,18,21,21,21,21,23,20,18,17] },
{ name: "d2", color: "var(--ui-error)", data: [7,9,9,12,13,16,19,19,24,24,26,25,26,24,24,25,25,22,23,22,22,19,16,15] },
{ name: "d3", color: "var(--ui-error)", data: [10,10,14,16,18,22,25,31,33,35,37,37,35,34,32,30,29,27,24,24,23,20,17,14] },
{ name: "d4", color: "var(--ui-error)", data: [10,13,14,20,24,30,34,39,44,47,47,47,48,44,42,38,32,31,28,24,21,17,16,14] },
{ name: "d5", color: "var(--ui-error)", data: [10,14,17,19,27,30,36,43,45,49,53,53,49,47,44,39,34,30,26,23,20,19,17,13] },
{ name: "d6", color: "var(--ui-error)", data: [9,13,14,18,22,28,32,39,41,44,48,48,45,44,38,34,32,27,23,20,19,15,14,12] },
{ name: "d7", color: "var(--ui-error)", data: [9,11,12,16,17,23,26,28,31,33,36,37,35,32,29,27,26,22,19,18,14,14,13,10] },
{ name: "d8", color: "var(--ui-error)", data: [7,9,9,12,13,16,18,20,22,21,22,23,24,23,20,18,17,15,16,13,12,12,10,9] },
{ name: "d9", color: "var(--ui-error)", data: [5,7,7,9,9,11,10,12,13,13,15,15,14,14,13,14,11,12,10,10,10,9,9,7] },
{ name: "d10", color: "var(--ui-error)", data: [5,6,6,5,7,7,6,7,8,7,10,9,9,10,10,9,8,8,8,7,7,7,6,5] },
{ name: "d11", color: "var(--ui-error)", data: [7,5,7,6,6,5,5,6,6,5,6,8,5,6,5,7,7,7,6,4,6,5,5,7] },
]}
terrainMode="dots"
terrainCallouts={false}
terrainDotColors={["var(--ui-primary)", "var(--ui-warning)", "var(--ui-error)"]}
format={(v) => `${v}m`}
/>
<!-- terrainCallouts={false} hides the highest/lowest titles, rail and
drops; terrainMode="dots" drops the mesh for a point-field look;
terrainDotColors bands the dots low/mid/high by elevation -->Box plot
| Category | Min | Q1 | Median | Q3 | Max |
|---|---|---|---|---|---|
| Starter | 42 | 68 | 84 | 102 | 128 |
| Growth | 58 | 92 | 118 | 148 | 178 |
| Scale | 96 | 148 | 182 | 218 | 262 |
<Chart
type="boxplot"
title="Response time by plan"
description="Whiskers span the adjacent values — dots past them are outliers"
labels={["Starter", "Growth", "Scale"]}
boxplots={[{ name: "Latency", boxes: [
{ min: 42, q1: 68, median: 84, q3: 102, max: 128 },
{ min: 58, q1: 92, median: 118, q3: 148, max: 178, outliers: [210, 226] },
{ min: 96, q1: 148, median: 182, q3: 218, max: 262 },
] }]}
/>
<!-- each box is { min, q1, median, q3, max, outliers? } aligned with
labels; a lone group colors one box per label, like pie slices -->Box plot from raw samples
| Category | Group | Min | Q1 | Median | Q3 | Max |
|---|---|---|---|---|---|---|
| Linux | Node 20 | 3.1 | 3.3 | 3.5 | 3.8 | 4.1 |
| macOS | Node 20 | 4.1 | 4.2 | 4.4 | 4.5 | 4.8 |
| Linux | Node 22 | 2.7 | 2.9 | 3 | 3.1 | 3.3 |
| macOS | Node 22 | 3.7 | 3.9 | 4.0 | 4.2 | 4.4 |
<Chart
type="boxplot"
title="Build minutes by runner"
description="Raw samples reduce to Tukey quartiles with 1.5×IQR fences at build time"
labels={["Linux", "macOS"]}
boxplots={[
{ name: "Node 20", boxes: [[3.1, 3.4, 3.2, 3.8, 3.5, 3.3, 3.6, 4.1, 3.4, 5.9], [4.2, 4.5, 4.1, 4.8, 4.4]] },
{ name: "Node 22", boxes: [[2.8, 3.0, 2.9, 3.2, 3.1, 2.7, 3.3, 3.0], [3.9, 4.0, 3.7, 4.2, 4.4, 4.1]] },
]}
/>
<!-- pass raw observations instead of a summary; several groups render
side-by-side per label and color per group, like bars -->API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | '"bar" | "line" | "area" | "pie" | "donut" | "pie3d" | "treemap" | "funnel" | "rose" | "histogram" | "waffle" | "bullet" | "dumbbell" | "scatter" | "hbar" | "waterfall" | "heatmap" | "cohort" | "terrain" | "map" | "boxplot"' | '"bar"' | Chart type. |
labels | string[] | required | Category labels (x axis; slice names for pie/donut). |
series | ChartSeries[] | required | Data series: { name, data, color? }. |
title | string | — | Accessible title, rendered as figcaption. |
description | string | — | Supporting line under the title. |
grid | boolean | true | Grid lines on cartesian charts. |
legend | boolean | true | Legend under the chart. |
innerRadius | number | 58 | Donut hole percentage. |
padAngle | number | 0 | Gap between pie/donut slices, in degrees. |
sliceRadius | number | 0 | Corner rounding of pie/donut slices, in px. |
sliceCorners | "all" | "outer" | "all" | Which slice corners sliceRadius rounds — every corner, or rim corners only with a sharp center tip (petal slices). |
sliceLabels | "none" | "value" | "value-pct" | "label" | "none" | Slice callouts: values, value·pct at centroids, or names outside with leader lines. |
sliceLabelFormat | (value: number, pct: number) => string | — | Custom text for value-style slice callouts. |
collapseBelow | number | 0 | Merge pie/donut slices below this percent share into a trailing muted Other slice (hover lists the contents). |
depth | number | 36 | 3D pie wall height in px (clamped 0–120) — chunkier extrusions read more dimensional; 0 is a flat tilted ellipse. |
tileRadius | number | 10 | Treemap tile corner rounding in px (clamped to half the tile's smallest side). |
roseMode | "area" | "radius" | "area" | Rose wedge encoding — area-proportional (Nightingale's manner) or radius-proportional. |
bins | number | Sturges | Histogram bin count override (1–40); defaults to ceil(log2(n) + 1) over the sample count. |
barGap | number | 2 | Histogram gutter between bars in px (never more than half a bar); 0 restores touching bars. |
bullets | BulletRow[] | — | Bullet rows: { label, ranges (ascending ceilings, 1–5), measure, target?, color? }. |
radialSweep | "full" | "half" | "full" | Ring sweep for radial charts — full circle or half-circle rainbow. |
nodes | SankeyNode[] | — | Sankey nodes: { name, color? }. |
links | SankeyLink[] | — | Sankey flows: { source, target, value, color? }. |
maxY | number | data max | Y-axis ceiling override (per-label totals max when stacked). |
rightMaxY | number | right-axis data max | Secondary-axis ceiling in combo charts. |
curve | boolean | "step" | "bump" | false | Smooth monotone curves (true), stepped plateaus ("step"), or slope-style S-curves ("bump") instead of straight segments. |
stroke | "solid" | "dashed" | "dotted" | "solid" | Line stroke style; a per-series stroke overrides it. |
fill | "solid" | "dots" | "hatch" | "solid" | Area fill paint — texture patterns tinted per series color; per-series fill (also "none") overrides. |
gradient | boolean | false | Gradient fills: area fades toward the baseline, bars per bar, pie/donut rim-strong radial fade. |
ticks | number | 36 | Tick count around the gauge arc (gauge only, clamped 8–80). |
barStyle | "rect" | "pill" | "rect" | Rectangular or thin fully-rounded pill columns (bar). |
gaugeStyle | "line" | "block" | "line" | Thin ticks or thick rounded block segments (gauge). |
gaugeRamp | string[] | — | 2+ CSS colors interpolated across the gauge arc (block style). |
gradientIntensity | number | 1 | Multiplier on gradient fade strength (clamped 0.1–2.5) — applies to every gradient type. |
stack | boolean | "percent" | false | Stack bar series (cumulative totals) or normalize each column to 100% with "percent". |
waterfallTotals | number[] | — | Waterfall: label indices that render as full total bars and reset the running sum. |
waterfallColors | [string, string, string] | success / error / primary | Waterfall colors for [increase, decrease, total] bars. |
heatmapRamp | string[] | muted → primary | 2+ CSS colors the heatmap cells interpolate across (data min → max). |
heatmapLabels | boolean | false | Print the formatted value inside each heatmap cell. |
cohortSizes | number[] | — | Cohort sizes; renders a users sublabel under each row name. |
cohortValues | "percent" | "value" | "percent" | Cohort cell text: share of cohort, or headcount via cohortSizes. |
cohortMax | number | data max | Ceiling for the cohort color ramp (fractions vs percents disambiguated automatically). |
cohortRamp | string[] | muted → primary | 2+ CSS colors the cohort cells interpolate across. |
terrainMode | "wire" | "dots" | "both" | "both" | Terrain rendering: mesh, bare point field, or both. |
terrainAxes | [string, string, string] | — | Terrain axis captions along the bottom (x, y, z). |
terrainCallouts | boolean | true | Highest/lowest corner titles, rail and drops; false hides them. |
terrainCalloutLabels | [string, string] | Highest / Lowest point | Custom corner-title texts for the extreme-point callouts. |
terrainDotColors | [string, string, string] | muted two-tone | Dot colors for [low, middle, high] elevation bands (thirds of the range). |
mapRegions | MapRegion[] | — | Map regions: { name, at: [col, row], span?, points?, value? } — rect tiles or freeform polygons. |
mapRamp | string[] | muted → primary | 2+ CSS colors the map region fills interpolate across (data min → max). |
mapLabels | boolean | false | Print the formatted value inside each map region. |
mapColumns | number | rect extent | Grid column count the tile cells divide — reserve columns for freeform polygon regions so rect tiles don't stretch. |
boxplots | BoxplotGroup[] | — | Boxplot groups: { name, color?, boxes } — one box per label, either a { min, q1, median, q3, max, outliers?, color? } summary or raw observations reduced to Tukey quartiles (1.5×IQR fences) at build time. |
boxRadius | number | 8 | Corner rounding of each boxplot box, in px. |
boxplotOutliers | boolean | true | Render outlier dots past the boxplot whiskers (still listed in tooltips and the data table when hidden). |
zoomable | boolean | false | Map: zoom buttons (+/−/Reset), wheel-to-zoom toward the cursor and drag-to-pan. The only chart feature that ships JS. |
format | (value: number) => string | — | Value formatting for ticks and the data table. |
height | number | 260 | Plot height in px. |
class | string | — | Merged via cn(); Tailwind classes welcome. |
id | string | — | Element id. |
Accessibility
Implemented accessibility features — semantics, ARIA wiring, and keyboard behavior that ship with the component.
- SVG carries role=img with a full aria-label summary
- Every bar, point, and slice has a native <title> hover tooltip
- A visually hidden <table> mirrors the data for screen readers
FAQ
Why no tooltips like library charts?
Zero JS is the point. Every mark carries a native <title>, so browsers show the value on hover — and screen readers read the hidden data table regardless.
How do I theme the colors?
Series colors fall back to --ui-chart-1..6 (then --ui-primary and friends). Override the tokens globally, or pass color per series for explicit control.
Is the data accessible?
Three ways: the svg has role=img with a summary aria-label, a visually hidden table mirrors every value, and each mark has a <title>.
Changelog
Version history mentioning Chart.
- Data display (10): Badge, Avatar, Attachment, Table, Progress, Bubble, Chart, Empty, Tag, Timeline.
- Dialog, Lightbox, Sheet, and AlertDialog backdrops, Progress stripes, Carousel shadows, and Chart SVG shadows resolve through tokens instead of hard-coded colors.