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

Revenue growthMonthly revenue, 2026
0100200300JanFebMarAprMayJunJan — Revenue: 247Feb — Revenue: 255Mar — Revenue: 208Apr — Revenue: 204May — Revenue: 176Jun — Revenue: 164
CategoryRevenue
Jan247
Feb255
Mar208
Apr204
May176
Jun164
<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

Desktop vs mobile
0100200300JanFebMarAprJan — Desktop: 245Jan — Mobile: 113Feb — Desktop: 198Feb — Mobile: 159Mar — Desktop: 193Mar — Mobile: 194Apr — Desktop: 248Apr — Mobile: 204
CategoryDesktopMobile
Jan245113
Feb198159
Mar193194
Apr248204
<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

Sessions by sourceStacked per day — axis spans daily totals
04008001200MonTueWedThuFriSatSunMon — Direct: 320 (total: 720)Mon — Search: 280 (total: 720)Mon — Social: 120 (total: 720)Tue — Direct: 360 (total: 800)Tue — Search: 300 (total: 800)Tue — Social: 140 (total: 800)Wed — Direct: 310 (total: 760)Wed — Search: 340 (total: 760)Wed — Social: 110 (total: 760)Thu — Direct: 420 (total: 960)Thu — Search: 380 (total: 960)Thu — Social: 160 (total: 960)Fri — Direct: 480 (total: 1080)Fri — Search: 410 (total: 1080)Fri — Social: 190 (total: 1080)Sat — Direct: 260 (total: 720)Sat — Search: 220 (total: 720)Sat — Social: 240 (total: 720)Sun — Direct: 210 (total: 590)Sun — Search: 180 (total: 590)Sun — Social: 200 (total: 590)
CategoryDirectSearchSocial
Mon320280120
Tue360300140
Wed310340110
Thu420380160
Fri480410190
Sat260220240
Sun210180200
<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

Traffic mixEach column normalized to 100% of its daily total
0255075100MonTueWedThuFriMon — Direct: 320 (44%)Mon — Search: 280 (39%)Mon — Social: 120 (17%)Tue — Direct: 360 (45%)Tue — Search: 300 (38%)Tue — Social: 140 (18%)Wed — Direct: 310 (41%)Wed — Search: 340 (45%)Wed — Social: 110 (14%)Thu — Direct: 420 (44%)Thu — Search: 380 (40%)Thu — Social: 160 (17%)Fri — Direct: 480 (44%)Fri — Search: 410 (38%)Fri — Social: 190 (18%)
CategoryDirectSearchSocial
Mon320280120
Tue360300140
Wed310340110
Thu420380160
Fri480410190
<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

Deploys per weekEach bar fades toward the baseline
0153045W1W2W3W4W5W6W1 — Deploys: 14W2 — Deploys: 22W3 — Deploys: 18W4 — Deploys: 27W5 — Deploys: 31W6 — Deploys: 24
CategoryDeploys
W114
W222
W318
W427
W531
W624
<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

PulseThin rounded columns — an equalizer look
020406012345678910111213141516171819201 — Load: 82 — Load: 143 — Load: 224 — Load: 305 — Load: 386 — Load: 447 — Load: 408 — Load: 329 — Load: 2010 — Load: 1011 — Load: 1612 — Load: 2813 — Load: 4014 — Load: 4615 — Load: 4216 — Load: 3017 — Load: 1818 — Load: 1019 — Load: 2620 — Load: 44
CategoryLoad
18
214
322
430
538
644
740
832
920
1010
1116
1228
1340
1446
1542
1630
1718
1810
1926
2044
<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)

Orders vs revenueBars ride the left axis, the line a secondary right axis
0250050007500080160240Oct 1Oct 2Oct 3Oct 4Oct 5Oct 6Oct 7Oct 8Oct 9Oct 1 — Revenue: 4200Oct 2 — Revenue: 4600Oct 3 — Revenue: 5100Oct 4 — Revenue: 4900Oct 5 — Revenue: 5600Oct 6 — Revenue: 6100Oct 7 — Revenue: 6900Oct 8 — Revenue: 5800Oct 9 — Revenue: 5400Oct 1 — Orders: 148Oct 2 — Orders: 158Oct 3 — Orders: 172Oct 4 — Orders: 165Oct 5 — Orders: 184Oct 6 — Orders: 196Oct 7 — Orders: 210Oct 8 — Orders: 186Oct 9 — Orders: 178
CategoryRevenueOrders
Oct 14200148
Oct 24600158
Oct 35100172
Oct 44900165
Oct 55600184
Oct 66100196
Oct 76900210
Oct 85800186
Oct 95400178
<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

Active users
0250500750MonTueWedThuFriSatSunMon — This week: 500Tue — This week: 524Wed — This week: 586Thu — This week: 552Fri — This week: 546Sat — This week: 539Sun — This week: 588Mon — Last week: 610Tue — Last week: 533Wed — Last week: 587Thu — Last week: 592Fri — Last week: 611Sat — Last week: 607Sun — Last week: 655
CategoryThis weekLast week
Mon500610
Tue524533
Wed586587
Thu552592
Fri546611
Sat539607
Sun588655
<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

Visitor flowMonotone cubic smoothing — the curve never overshoots the data
0100200300JanFebMarAprMayJunJan — Visitor-line: 247Feb — Visitor-line: 250Mar — Visitor-line: 247Apr — Visitor-line: 209May — Visitor-line: 191Jun — Visitor-line: 260
CategoryVisitor-line
Jan247
Feb250
Mar247
Apr209
May191
Jun260
<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

Desktop vs mobilePlateaus hold until the midpoint between values, then step
0300600900JanFebMarAprMayJunJulAugSepOctNovDecJan — Desktop: 420Feb — Desktop: 615Mar — Desktop: 545Apr — Desktop: 760May — Desktop: 480Jun — Desktop: 825Jul — Desktop: 570Aug — Desktop: 690Sep — Desktop: 515Oct — Desktop: 735Nov — Desktop: 610Dec — Desktop: 455Jan — Mobile: 265Feb — Mobile: 430Mar — Mobile: 350Apr — Mobile: 505May — Mobile: 390Jun — Mobile: 545Jul — Mobile: 415Aug — Mobile: 585Sep — Mobile: 445Oct — Mobile: 615Nov — Mobile: 520Dec — Mobile: 380
CategoryDesktopMobile
Jan420265
Feb615430
Mar545350
Apr760505
May480390
Jun825545
Jul570415
Aug690585
Sep515445
Oct735615
Nov610520
Dec455380
<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

Rank changesFlat out of each point, S-curve into the next
0200400600JanFebMarAprMayJunJan — Desktop: 320Feb — Desktop: 180Mar — Desktop: 420Apr — Desktop: 260May — Desktop: 510Jun — Desktop: 380Jan — Mobile: 180Feb — Mobile: 320Mar — Mobile: 240Apr — Mobile: 450May — Mobile: 290Jun — Mobile: 460
CategoryDesktopMobile
Jan320180
Feb180320
Mar420240
Apr260450
May510290
Jun380460
<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

ForecastWhole series dashed — chart-level stroke, per-series override
0200400600Apr 1Apr 5Apr 10Apr 15Apr 18Apr 1 — Sessions: 597Apr 5 — Sessions: 488Apr 10 — Sessions: 386Apr 15 — Sessions: 455Apr 18 — Sessions: 480
CategorySessions
Apr 1597
Apr 5488
Apr 10386
Apr 15455
Apr 18480
Solid vs dashed vs dotted
0200400600JanFebMarAprJan — Actual: 535Feb — Actual: 568Mar — Actual: 554Apr — Actual: 586Jan — Target: 395Feb — Target: 388Mar — Target: 380Apr — Target: 437Jan — Stretch: 380Feb — Stretch: 345Mar — Stretch: 337Apr — Stretch: 369
CategoryActualTargetStretch
Jan535395380
Feb568388345
Mar554380337
Apr586437369
<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

Portfolio valuegradient on a line renders a whisper-soft fade under the stroke
0255075JanFebMarAprMayJunJulAugSepOctNovDecJan — NAV: 42Feb — NAV: 45Mar — NAV: 44Apr — NAV: 48May — NAV: 52Jun — NAV: 50Jul — NAV: 55Aug — NAV: 58Sep — NAV: 57Oct — NAV: 62Nov — NAV: 66Dec — NAV: 71
CategoryNAV
Jan42
Feb45
Mar44
Apr48
May52
Jun50
Jul55
Aug58
Sep57
Oct62
Nov66
Dec71
<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)

Ping
12am6am12pm6pm12am — ms: 496am — ms: 5412pm — ms: 546pm — ms: 50
Categoryms
12am49
6am54
12pm54
6pm50
<Chart
  type="line"
  grid={false}
  legend={false}
  title="Ping"
  labels={["12am", "6am", "12pm", "6pm"]}
  series={[{ name: "ms", data: [49,54,54,50] ]}
/>

Area

Signups
080016002400Q1Q2Q3Q4Q1 — Signups: 1591Q2 — Signups: 1994Q3 — Signups: 1976Q4 — Signups: 1672
CategorySignups
Q11591
Q21994
Q31976
Q41672
<Chart
  type="area"
  title="Signups"
  labels={["Q1", "Q2", "Q3", "Q4"]}
  series={[{ name: "Signups", data: [1591,1994,1976,1672] ]}
/>

Curved area

Campaign trafficSmoothed area — the fill follows the monotone curve
050010001500JanFebMarAprMayJunJan — Sessions: 820Feb — Sessions: 1210Mar — Sessions: 940Apr — Sessions: 580May — Sessions: 1120Jun — Sessions: 990
CategorySessions
Jan820
Feb1210
Mar940
Apr580
May1120
Jun990
<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

API requestsSeries color fades toward the baseline
0250050007500MonTueWedThuFriSatSunMon — Requests: 4200Tue — Requests: 5100Wed — Requests: 4800Thu — Requests: 6300Fri — Requests: 7100Sat — Requests: 3900Sun — Requests: 3400
CategoryRequests
Mon4200
Tue5100
Wed4800
Thu6300
Fri7100
Sat3900
Sun3400
<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

RevenueThe series color drives the line, the fill, and the gradient
0102030JanFebMarAprMayJunJan — Revenue: 12Feb — Revenue: 18Mar — Revenue: 15Apr — Revenue: 22May — Revenue: 19Jun — Revenue: 26
CategoryRevenue
Jan12
Feb18
Mar15
Apr22
May19
Jun26
<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

Strong fadegradientIntensity scales every fade — 1 is the default, 2 is saturated
0250050007500MonTueWedThuFriSatSunMon — Requests: 4200Tue — Requests: 5100Wed — Requests: 4800Thu — Requests: 6300Fri — Requests: 7100Sat — Requests: 3900Sun — Requests: 3400
CategoryRequests
Mon4200
Tue5100
Wed4800
Thu6300
Fri7100
Sat3900
Sun3400
<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)

Portfolio swingTexture fills tinted with each series color
0150300450W1W2W3W4W5W6W7W8W1 — Fund A: 312W2 — Fund A: 388W3 — Fund A: 340W4 — Fund A: 296W5 — Fund A: 274W6 — Fund A: 246W7 — Fund A: 231W8 — Fund A: 224W1 — Fund B: 268W2 — Fund B: 231W3 — Fund B: 205W4 — Fund B: 214W5 — Fund B: 248W6 — Fund B: 289W7 — Fund B: 331W8 — Fund B: 352
CategoryFund AFund B
W1312268
W2388231
W3340205
W4296214
W5274248
W6246289
W7231331
W8224352
<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)

Weekly signups275 this week — +12.2% vs target
0150300450JanFebMarAprMayJunJan — Actual: 240Feb — Actual: 178Mar — Actual: 262Apr — Actual: 335May — Actual: 212Jun — Actual: 275Jan — Target: 122Feb — Target: 202Mar — Target: 128Apr — Target: 390May — Target: 305Jun — Target: 172
CategoryActualTarget
Jan240122
Feb178202
Mar262128
Apr335390
May212305
Jun275172
<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

Traffic sources
Direct: 435 (45%)Search: 286 (30%)Social: 151 (16%)Referral: 96 (10%)
CategoryShare
Direct435
Search286
Social151
Referral96
<Chart
  type="pie"
  title="Traffic sources"
  labels={["Direct", "Search", "Social", "Referral"]}
  series={[{ name: "Share", data: [435, 286, 151, 96] }]}
/>

Donut

Storage by type
Images: 42 (30%)Video: 66 (47%)Docs: 21 (15%)Other: 11 (8%)
CategoryGB
Images42
Video66
Docs21
Other11
<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)

Browser sharepadAngle gaps the slices, sliceRadius rounds their corners
Chrome: 38 (38%)Safari: 24 (24%)Firefox: 16 (16%)Edge: 12 (12%)Other: 10 (10%)
CategoryShare
Chrome38
Safari24
Firefox16
Edge12
Other10
<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)

Vote shareOnly the rim corners round — the center tip stays sharp
A: 28 (28%)B: 22 (22%)C: 18 (18%)D: 14 (14%)E: 10 (10%)F: 8 (8%)
CategoryVotes
A28
B22
C18
D14
E10
F8
<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)

Share of voiceExtruded slices with shaded walls and outside callouts
Other: 22 (22%)Signal OS: 21 (21%)Nova Chat: 16 (16%)Pulse: 15 (15%)Wave: 14 (14%)Orbit: 12 (12%)Other: 22 (22%)Signal OS: 21 (21%)Nova Chat: 16 (16%)Pulse: 15 (15%)Wave: 14 (14%)Orbit: 12 (12%)22% OTHER21% SIGNAL OS16% NOVA CHAT15% PULSE14% WAVE12% ORBIT
CategoryShare
Other22
Signal OS21
Nova Chat16
Pulse15
Wave14
Orbit12
<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

Top productsTile area scales with the value — squarified at build time
Other: 22 (22%)Other22Signal OS: 21 (21%)Signal OS21Nova Chat: 16 (16%)Nova Chat16Atlas AI: 15 (15%)Atlas AI15Assistly: 14 (14%)Assistly14Draft Lab: 12 (12%)Draft Lab12
CategoryUsers
Other22
Nova Chat16
Assistly14
Signal OS21
Atlas AI15
Draft Lab12
<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

Hiring pipelineStages keep input order — widths scale with the value
Applied: 1200 (45% of total)Applied1200 (45%)Screened: 780 (29% of total)Screened780 (29%)Interview: 430 (16% of total)Interview430 (16%)Offer: 180 (7% of total)Hired: 96 (4% of total)
CategoryCandidates
Applied1200
Screened780
Interview430
Offer180
Hired96
<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

Cases by monthEqual angles, area encodes the value — Nightingale's polar-area manner
Jan: 12 (8%)Feb: 19 (12%)Mar: 33 (21%)Apr: 27 (18%)May: 41 (27%)Jun: 22 (14%)121933274122
CategoryCases
Jan12
Feb19
Mar33
Apr27
May41
Jun22
<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

Response timeRaw samples auto-binned (Sturges) into touching bars
02.34.7712–28.3: 6 samples28.3–44.7: 6 samples44.7–61: 6 samples61–77.3: 7 samples77.3–93.7: 3 samples93.7–110: 2 samples1228.344.76177.393.7110
BinCount
12–28.36
28.3–44.76
44.7–616
61–77.37
77.3–93.73
93.7–1102
<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

Market share10×10 unit grid — each cell is 1% of the total
Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Alpha: 55 (55%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Beta: 30 (30%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)Gamma: 15 (15%)
CategoryShare
Alpha55
Beta30
Gamma15
<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

Q3 performanceActual vs target against qualitative ranges (Few's bullet graph)
0233.3466.7700Revenue: 270 (target 250) — ranges 150 / 225 / 300Revenue: target 250Revenue270Profit: 21 (target 26) — ranges 20 / 25 / 30Profit: target 26Profit21Orders: 640 — ranges 400 / 550 / 700Orders640
MeasureRangesActualTarget
Revenue150 / 225 / 300270250
Profit20 / 25 / 302126
Orders400 / 550 / 700640—
<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

Before vs afterPaired dots with a delta connector — one series is a dot plot
24.1385265.9Shop: Δ 19Shop — Before: 32Shop — After: 51Blog: Δ 4Blog — Before: 48Blog — After: 44Docs: Δ 36Docs — Before: 27Docs — After: 63ShopBlogDocs
CategoryBeforeAfter
Shop3251
Blog4844
Docs2763
<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)

Browser shareFormatted values printed at each slice centroid
Chrome: 275 (30%)Safari: 200 (22%)Firefox: 187 (20%)Edge: 173 (19%)Other: 90 (10%)27520018717390
CategoryUsers
Chrome275
Safari200
Firefox187
Edge173
Other90
Browser shareCategory names outside, connected by elbow leader lines
Chrome: 275 (30%)Safari: 200 (22%)Firefox: 187 (20%)Edge: 173 (19%)Other: 90 (10%)ChromeSafariFirefoxEdgeOther
CategoryUsers
Chrome275
Safari200
Firefox187
Edge173
Other90
<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

Plan distributionValue and share together at each slice centroid
Free: 412 (46%)Pro: 268 (30%)Team: 143 (16%)Enterprise: 77 (9%)412 · 46%268 · 30%143 · 16%77 · 9%
CategoryAccounts
Free412
Pro268
Team143
Enterprise77
Storage by typesliceLabelFormat takes (value, pct) and returns any string
Images: 42 (30%)Video: 66 (47%)Docs: 21 (15%)Other: 11 (8%)30% of 4247% of 6615% of 218% of 11
CategoryGB
Images42
Video66
Docs21
Other11
<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

Entry pagesSlices under 8% merge into the trailing Other slice
/: 4820 (46%)/pricing: 2130 (20%)/blog: 1640 (16%)/docs: 1180 (11%)Other (3): /changelog, /about, /careers4820 · 46%2130 · 20%1640 · 16%1180 · 11%730 · 7%
CategorySessions
/4820
/pricing2130
/blog1640
/docs1180
/changelog310
/about240
/careers180
<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

Storage by typeRadial fade — strong at the rim, clear toward the center
Images: 42 (30%)Video: 66 (47%)Docs: 21 (15%)Other: 11 (8%)
CategoryGB
Images42
Video66
Docs21
Other11
<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

Plan distribution
Free: 640 (62%)Pro: 310 (30%)Team: 90 (9%)
CategoryPlans
Free640
Pro310
Team90

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

Deploys per day
0369MonTueWedThuFriMon — Team A: 5Tue — Team A: 6Wed — Team A: 7Thu — Team A: 9Fri — Team A: 9Mon — Team B: 4Tue — Team B: 3Wed — Team B: 3Thu — Team B: 5Fri — Team B: 7
CategoryTeam ATeam B
Mon54
Tue63
Wed73
Thu95
Fri97
<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

Top pages
  • /docs/get-started3880
  • /components4706
  • /blocks3866
  • /docs/rtl2923
CategoryViews
/docs/get-started3880
/components4706
/blocks3866
/docs/rtl2923
<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)

Capital inflows
CategoryWaveMark CapitalEnvato FinancesQBridge Tech
Funds221015
<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

Storage usedTick marks fill clockwise with the value
64of 8 TB
Categoryof 8 TB
Used64
<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

Monthly goal
102of 200 deals
Categoryof 200 deals
Reached102
<Chart
  type="gauge"
  title="Monthly goal"
  labels={["Reached"]}
  series={[{ name: "of 200 deals", data: [102] }]}
  maxY={200}
  legend={false}
/>

Block gauge

Total expenses
$44,220of $80k budget
Categoryof $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

Browser shareOne concentric ring per category — outer ring first
Chrome: 275Safari: 200Firefox: 187Edge: 173Other: 90
CategoryUsers
Chrome275
Safari200
Firefox187
Edge173
Other90
<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)

Browser shareHalf-circle rings over the top — outer ring first
Chrome: 275Safari: 200Firefox: 187Edge: 173Other: 90
CategoryUsers
Chrome275
Safari200
Firefox187
Edge173
Other90
<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

Skill coverageOne axis per label, filled polygons per series
JavaScriptPythonCSSNode.jsReactTypeScriptJavaScript — Desktop: 78Python — Desktop: 85CSS — Desktop: 90Node.js — Desktop: 62React — Desktop: 74TypeScript — Desktop: 96JavaScript — Mobile: 52Python — Mobile: 58CSS — Mobile: 66Node.js — Mobile: 54React — Mobile: 41TypeScript — Mobile: 60
CategoryDesktopMobile
JavaScript7852
Python8558
CSS9066
Node.js6254
React7441
TypeScript9660
<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

Traffic funnelFlow value scales each ribbon and node
Organic Search → Landing Page: 42000Paid Ads → Landing Page: 28000Social Media → Landing Page: 18000Landing Page → Product Page: 52000Landing Page → Bounced: 36000Product Page → Cart: 31000Product Page → Bounced: 21000Cart → Purchase: 24000Cart → Bounced: 7000Organic Search: 42000Organic Search42000Paid Ads: 28000Paid Ads28000Social Media: 18000Social Media18000Landing Page: 88000Landing Page88000Product Page: 52000Product Page52000Cart: 31000Cart31000Bounced: 64000Bounced64000Purchase: 24000Purchase24000
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

Cash flow, 2026Signed deltas stack on a running total; totals anchor to zero
$0k$68.3k$136.7k$205kStartQ1Q2Q3Q4EndStart: total $120kQ1: +$45k → $165kQ2: −$25k → $165kQ3: +$65k → $205kQ4: −$20k → $205kEnd: total $185k
CategoryCash
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

Commit activityRows are days, columns are week offsets — darker means more commits
Mon — -5w: 4Mon — -4w: 9Mon — -3w: 6Mon — -2w: 12Mon — -1w: 8Mon — This week: 11Wed — -5w: 7Wed — -4w: 5Wed — -3w: 10Wed — -2w: 8Wed — -1w: 13Wed — This week: 9Fri — -5w: 12Fri — -4w: 14Fri — -3w: 9Fri — -2w: 15Fri — -1w: 11Fri — This week: 16Sat — -5w: 2Sat — -4w: 1Sat — -3w: 3Sat — -2w: 0Sat — -1w: 2Sat — This week: 5496128117510813912149151116213025-5w-4w-3w-2w-1wThis weekMonWedFriSat
CategoryMonWedFriSat
-5w47122
-4w95141
-3w61093
-2w128150
-1w813112
This week119165
<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

Weekly signup retentionRows are signup cohorts, columns are weeks since signup — the empty tail hasn't been observed yet
Sep 1 — Week 0: 100% of cohortSep 1 — Week 1: 41% of cohortSep 1 — Week 2: 33% of cohortSep 1 — Week 3: 29% of cohortSep 1 — Week 4: 27% of cohortSep 8 — Week 0: 100% of cohortSep 8 — Week 1: 39% of cohortSep 8 — Week 2: 31% of cohortSep 8 — Week 3: 28% of cohortSep 8 — Week 4: 26% of cohortSep 15 — Week 0: 100% of cohortSep 15 — Week 1: 44% of cohortSep 15 — Week 2: 36% of cohortSep 15 — Week 3: 33% of cohortSep 22 — Week 0: 100% of cohortSep 22 — Week 1: 47% of cohortSep 22 — Week 2: 40% of cohort100%41%33%29%27%100%39%31%28%26%100%44%36%33%100%47%40%Week 0Week 1Week 2Week 3Week 4Sep 1480 usersSep 8510 usersSep 15465 usersSep 22530 users
CategorySep 1Sep 8Sep 15Sep 22
Week 01111
Week 10.40.40.40.5
Week 20.30.30.40.4
Week 30.30.30.30
Week 40.30.300
<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)

Regional adoptionAdoption intensity across operating regions
Northwest: 55North Central: 96Southwest: 8South Central: 82East: 45
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)

Revenue by stateScroll to zoom, drag to pan — neighboring polygons share boundary vertices, so no gaps
Washington: 68Idaho: 22Oregon: 42California: 94Nevada: 36Montana: 18Wyoming: 14Utah: 28Colorado: 52Arizona: 46New Mexico: 24North Dakota: 12South Dakota: 16Nebraska: 20Kansas: 26Texas: 84Minnesota: 24Iowa: 22Missouri: 32Wisconsin: 26Arkansas: 30Louisiana: 28Ohio: 56Kentucky: 36Tennessee: 34Mississippi: 22Alabama: 30New York: 74Maine: 18Pennsylvania: 62New Jersey: 70West Virginia: 20Virginia: 44North Carolina: 46South Carolina: 32Georgia: 48Florida: 66
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

Signal terrainPeaks and valleys across a sampled field
d0 — -12: 5md0 — -11: 6md0 — -10: 8md0 — -9: 6md0 — -8: 8md0 — -7: 8md0 — -6: 9md0 — -5: 7md0 — -4: 8md0 — -3: 11md0 — -2: 9md0 — -1: 12md0 — 0: 12md0 — 1: 14md0 — 2: 16md0 — 3: 16md0 — 4: 19md0 — 5: 20md0 — 6: 20md0 — 7: 22md0 — 8: 22md0 — 9: 21md0 — 10: 19md0 — 11: 18md1 — -12: 7md1 — -11: 7md1 — -10: 8md1 — -9: 9md1 — -8: 8md1 — -7: 9md1 — -6: 10md1 — -5: 14md1 — -4: 13md1 — -3: 16md1 — -2: 16md1 — -1: 16md1 — 0: 15md1 — 1: 16md1 — 2: 17md1 — 3: 18md1 — 4: 21md1 — 5: 21md1 — 6: 21md1 — 7: 21md1 — 8: 23md1 — 9: 20md1 — 10: 18md1 — 11: 17md2 — -12: 7md2 — -11: 9md2 — -10: 9md2 — -9: 12md2 — -8: 13md2 — -7: 16md2 — -6: 19md2 — -5: 19md2 — -4: 24md2 — -3: 24md2 — -2: 26md2 — -1: 25md2 — 0: 26md2 — 1: 24md2 — 2: 24md2 — 3: 25md2 — 4: 25md2 — 5: 22md2 — 6: 23md2 — 7: 22md2 — 8: 22md2 — 9: 19md2 — 10: 16md2 — 11: 15md3 — -12: 10md3 — -11: 10md3 — -10: 14md3 — -9: 16md3 — -8: 18md3 — -7: 22md3 — -6: 25md3 — -5: 31md3 — -4: 33md3 — -3: 35md3 — -2: 37md3 — -1: 37md3 — 0: 35md3 — 1: 34md3 — 2: 32md3 — 3: 30md3 — 4: 29md3 — 5: 27md3 — 6: 24md3 — 7: 24md3 — 8: 23md3 — 9: 20md3 — 10: 17md3 — 11: 14md4 — -12: 10md4 — -11: 13md4 — -10: 14md4 — -9: 20md4 — -8: 24md4 — -7: 30md4 — -6: 34md4 — -5: 39md4 — -4: 44md4 — -3: 47md4 — -2: 47md4 — -1: 47md4 — 0: 48md4 — 1: 44md4 — 2: 42md4 — 3: 38md4 — 4: 32md4 — 5: 31md4 — 6: 28md4 — 7: 24md4 — 8: 21md4 — 9: 17md4 — 10: 16md4 — 11: 14md5 — -12: 10md5 — -11: 14md5 — -10: 17md5 — -9: 19md5 — -8: 27md5 — -7: 30md5 — -6: 36md5 — -5: 43md5 — -4: 45md5 — -3: 49md5 — -2: 53md5 — -1: 53md5 — 0: 49md5 — 1: 47md5 — 2: 44md5 — 3: 39md5 — 4: 34md5 — 5: 30md5 — 6: 26md5 — 7: 23md5 — 8: 20md5 — 9: 19md5 — 10: 17md5 — 11: 13md6 — -12: 9md6 — -11: 13md6 — -10: 14md6 — -9: 18md6 — -8: 22md6 — -7: 28md6 — -6: 32md6 — -5: 39md6 — -4: 41md6 — -3: 44md6 — -2: 48md6 — -1: 48md6 — 0: 45md6 — 1: 44md6 — 2: 38md6 — 3: 34md6 — 4: 32md6 — 5: 27md6 — 6: 23md6 — 7: 20md6 — 8: 19md6 — 9: 15md6 — 10: 14md6 — 11: 12md7 — -12: 9md7 — -11: 11md7 — -10: 12md7 — -9: 16md7 — -8: 17md7 — -7: 23md7 — -6: 26md7 — -5: 28md7 — -4: 31md7 — -3: 33md7 — -2: 36md7 — -1: 37md7 — 0: 35md7 — 1: 32md7 — 2: 29md7 — 3: 27md7 — 4: 26md7 — 5: 22md7 — 6: 19md7 — 7: 18md7 — 8: 14md7 — 9: 14md7 — 10: 13md7 — 11: 10md8 — -12: 7md8 — -11: 9md8 — -10: 9md8 — -9: 12md8 — -8: 13md8 — -7: 16md8 — -6: 18md8 — -5: 20md8 — -4: 22md8 — -3: 21md8 — -2: 22md8 — -1: 23md8 — 0: 24md8 — 1: 23md8 — 2: 20md8 — 3: 18md8 — 4: 17md8 — 5: 15md8 — 6: 16md8 — 7: 13md8 — 8: 12md8 — 9: 12md8 — 10: 10md8 — 11: 9md9 — -12: 5md9 — -11: 7md9 — -10: 7md9 — -9: 9md9 — -8: 9md9 — -7: 11md9 — -6: 10md9 — -5: 12md9 — -4: 13md9 — -3: 13md9 — -2: 15md9 — -1: 15md9 — 0: 14md9 — 1: 14md9 — 2: 13md9 — 3: 14md9 — 4: 11md9 — 5: 12md9 — 6: 10md9 — 7: 10md9 — 8: 10md9 — 9: 9md9 — 10: 9md9 — 11: 7md10 — -12: 5md10 — -11: 6md10 — -10: 6md10 — -9: 5md10 — -8: 7md10 — -7: 7md10 — -6: 6md10 — -5: 7md10 — -4: 8md10 — -3: 7md10 — -2: 10md10 — -1: 9md10 — 0: 9md10 — 1: 10md10 — 2: 10md10 — 3: 9md10 — 4: 8md10 — 5: 8md10 — 6: 8md10 — 7: 7md10 — 8: 7md10 — 9: 7md10 — 10: 6md10 — 11: 5md11 — -12: 7md11 — -11: 5md11 — -10: 7md11 — -9: 6md11 — -8: 6md11 — -7: 5md11 — -6: 5md11 — -5: 6md11 — -4: 6md11 — -3: 5md11 — -2: 6md11 — -1: 8md11 — 0: 5md11 — 1: 6md11 — 2: 5md11 — 3: 7md11 — 4: 7md11 — 5: 7md11 — 6: 6md11 — 7: 4md11 — 8: 6md11 — 9: 5md11 — 10: 5md11 — 11: 7md1 — -10: 8md1 — -6: 10md1 — -2: 16md1 — 2: 17md1 — 6: 21md1 — 10: 18md4 — -10: 14md4 — -6: 34md4 — -2: 47md4 — 2: 42md4 — 6: 28md4 — 10: 16md7 — -10: 12md7 — -6: 26md7 — -2: 36md7 — 2: 29md7 — 6: 19md7 — 10: 13md10 — -10: 6md10 — -6: 6md10 — -2: 10md10 — 2: 10md10 — 6: 8md10 — 10: 6mHighest point53mLowest point4mX: PositionY: ElevationZ: Depth
Categoryd0d1d2d3d4d5d6d7d8d9d10d11
-125m7m7m10m10m10m9m9m7m5m5m7m
-116m7m9m10m13m14m13m11m9m7m6m5m
-108m8m9m14m14m17m14m12m9m7m6m7m
-96m9m12m16m20m19m18m16m12m9m5m6m
-88m8m13m18m24m27m22m17m13m9m7m6m
-78m9m16m22m30m30m28m23m16m11m7m5m
-69m10m19m25m34m36m32m26m18m10m6m5m
-57m14m19m31m39m43m39m28m20m12m7m6m
-48m13m24m33m44m45m41m31m22m13m8m6m
-311m16m24m35m47m49m44m33m21m13m7m5m
-29m16m26m37m47m53m48m36m22m15m10m6m
-112m16m25m37m47m53m48m37m23m15m9m8m
012m15m26m35m48m49m45m35m24m14m9m5m
114m16m24m34m44m47m44m32m23m14m10m6m
216m17m24m32m42m44m38m29m20m13m10m5m
316m18m25m30m38m39m34m27m18m14m9m7m
419m21m25m29m32m34m32m26m17m11m8m7m
520m21m22m27m31m30m27m22m15m12m8m7m
620m21m23m24m28m26m23m19m16m10m8m6m
722m21m22m24m24m23m20m18m13m10m7m4m
822m23m22m23m21m20m19m14m12m10m7m6m
921m20m19m20m17m19m15m14m12m9m7m5m
1019m18m16m17m16m17m14m13m10m9m6m5m
1118m17m15m14m14m13m12m10m9m7m5m7m
<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)

Signal terrain, bareDots banded by elevation — primary low, warning mid, error high
d0 — -12: 5md0 — -11: 6md0 — -10: 8md0 — -9: 6md0 — -8: 8md0 — -7: 8md0 — -6: 9md0 — -5: 7md0 — -4: 8md0 — -3: 11md0 — -2: 9md0 — -1: 12md0 — 0: 12md0 — 1: 14md0 — 2: 16md0 — 3: 16md0 — 4: 19md0 — 5: 20md0 — 6: 20md0 — 7: 22md0 — 8: 22md0 — 9: 21md0 — 10: 19md0 — 11: 18md1 — -12: 7md1 — -11: 7md1 — -10: 8md1 — -9: 9md1 — -8: 8md1 — -7: 9md1 — -6: 10md1 — -5: 14md1 — -4: 13md1 — -3: 16md1 — -2: 16md1 — -1: 16md1 — 0: 15md1 — 1: 16md1 — 2: 17md1 — 3: 18md1 — 4: 21md1 — 5: 21md1 — 6: 21md1 — 7: 21md1 — 8: 23md1 — 9: 20md1 — 10: 18md1 — 11: 17md2 — -12: 7md2 — -11: 9md2 — -10: 9md2 — -9: 12md2 — -8: 13md2 — -7: 16md2 — -6: 19md2 — -5: 19md2 — -4: 24md2 — -3: 24md2 — -2: 26md2 — -1: 25md2 — 0: 26md2 — 1: 24md2 — 2: 24md2 — 3: 25md2 — 4: 25md2 — 5: 22md2 — 6: 23md2 — 7: 22md2 — 8: 22md2 — 9: 19md2 — 10: 16md2 — 11: 15md3 — -12: 10md3 — -11: 10md3 — -10: 14md3 — -9: 16md3 — -8: 18md3 — -7: 22md3 — -6: 25md3 — -5: 31md3 — -4: 33md3 — -3: 35md3 — -2: 37md3 — -1: 37md3 — 0: 35md3 — 1: 34md3 — 2: 32md3 — 3: 30md3 — 4: 29md3 — 5: 27md3 — 6: 24md3 — 7: 24md3 — 8: 23md3 — 9: 20md3 — 10: 17md3 — 11: 14md4 — -12: 10md4 — -11: 13md4 — -10: 14md4 — -9: 20md4 — -8: 24md4 — -7: 30md4 — -6: 34md4 — -5: 39md4 — -4: 44md4 — -3: 47md4 — -2: 47md4 — -1: 47md4 — 0: 48md4 — 1: 44md4 — 2: 42md4 — 3: 38md4 — 4: 32md4 — 5: 31md4 — 6: 28md4 — 7: 24md4 — 8: 21md4 — 9: 17md4 — 10: 16md4 — 11: 14md5 — -12: 10md5 — -11: 14md5 — -10: 17md5 — -9: 19md5 — -8: 27md5 — -7: 30md5 — -6: 36md5 — -5: 43md5 — -4: 45md5 — -3: 49md5 — -2: 53md5 — -1: 53md5 — 0: 49md5 — 1: 47md5 — 2: 44md5 — 3: 39md5 — 4: 34md5 — 5: 30md5 — 6: 26md5 — 7: 23md5 — 8: 20md5 — 9: 19md5 — 10: 17md5 — 11: 13md6 — -12: 9md6 — -11: 13md6 — -10: 14md6 — -9: 18md6 — -8: 22md6 — -7: 28md6 — -6: 32md6 — -5: 39md6 — -4: 41md6 — -3: 44md6 — -2: 48md6 — -1: 48md6 — 0: 45md6 — 1: 44md6 — 2: 38md6 — 3: 34md6 — 4: 32md6 — 5: 27md6 — 6: 23md6 — 7: 20md6 — 8: 19md6 — 9: 15md6 — 10: 14md6 — 11: 12md7 — -12: 9md7 — -11: 11md7 — -10: 12md7 — -9: 16md7 — -8: 17md7 — -7: 23md7 — -6: 26md7 — -5: 28md7 — -4: 31md7 — -3: 33md7 — -2: 36md7 — -1: 37md7 — 0: 35md7 — 1: 32md7 — 2: 29md7 — 3: 27md7 — 4: 26md7 — 5: 22md7 — 6: 19md7 — 7: 18md7 — 8: 14md7 — 9: 14md7 — 10: 13md7 — 11: 10md8 — -12: 7md8 — -11: 9md8 — -10: 9md8 — -9: 12md8 — -8: 13md8 — -7: 16md8 — -6: 18md8 — -5: 20md8 — -4: 22md8 — -3: 21md8 — -2: 22md8 — -1: 23md8 — 0: 24md8 — 1: 23md8 — 2: 20md8 — 3: 18md8 — 4: 17md8 — 5: 15md8 — 6: 16md8 — 7: 13md8 — 8: 12md8 — 9: 12md8 — 10: 10md8 — 11: 9md9 — -12: 5md9 — -11: 7md9 — -10: 7md9 — -9: 9md9 — -8: 9md9 — -7: 11md9 — -6: 10md9 — -5: 12md9 — -4: 13md9 — -3: 13md9 — -2: 15md9 — -1: 15md9 — 0: 14md9 — 1: 14md9 — 2: 13md9 — 3: 14md9 — 4: 11md9 — 5: 12md9 — 6: 10md9 — 7: 10md9 — 8: 10md9 — 9: 9md9 — 10: 9md9 — 11: 7md10 — -12: 5md10 — -11: 6md10 — -10: 6md10 — -9: 5md10 — -8: 7md10 — -7: 7md10 — -6: 6md10 — -5: 7md10 — -4: 8md10 — -3: 7md10 — -2: 10md10 — -1: 9md10 — 0: 9md10 — 1: 10md10 — 2: 10md10 — 3: 9md10 — 4: 8md10 — 5: 8md10 — 6: 8md10 — 7: 7md10 — 8: 7md10 — 9: 7md10 — 10: 6md10 — 11: 5md11 — -12: 7md11 — -11: 5md11 — -10: 7md11 — -9: 6md11 — -8: 6md11 — -7: 5md11 — -6: 5md11 — -5: 6md11 — -4: 6md11 — -3: 5md11 — -2: 6md11 — -1: 8md11 — 0: 5md11 — 1: 6md11 — 2: 5md11 — 3: 7md11 — 4: 7md11 — 5: 7md11 — 6: 6md11 — 7: 4md11 — 8: 6md11 — 9: 5md11 — 10: 5md11 — 11: 7md1 — -10: 8md1 — -6: 10md1 — -2: 16md1 — 2: 17md1 — 6: 21md1 — 10: 18md4 — -10: 14md4 — -6: 34md4 — -2: 47md4 — 2: 42md4 — 6: 28md4 — 10: 16md7 — -10: 12md7 — -6: 26md7 — -2: 36md7 — 2: 29md7 — 6: 19md7 — 10: 13md10 — -10: 6md10 — -6: 6md10 — -2: 10md10 — 2: 10md10 — 6: 8md10 — 10: 6m
Categoryd0d1d2d3d4d5d6d7d8d9d10d11
-125m7m7m10m10m10m9m9m7m5m5m7m
-116m7m9m10m13m14m13m11m9m7m6m5m
-108m8m9m14m14m17m14m12m9m7m6m7m
-96m9m12m16m20m19m18m16m12m9m5m6m
-88m8m13m18m24m27m22m17m13m9m7m6m
-78m9m16m22m30m30m28m23m16m11m7m5m
-69m10m19m25m34m36m32m26m18m10m6m5m
-57m14m19m31m39m43m39m28m20m12m7m6m
-48m13m24m33m44m45m41m31m22m13m8m6m
-311m16m24m35m47m49m44m33m21m13m7m5m
-29m16m26m37m47m53m48m36m22m15m10m6m
-112m16m25m37m47m53m48m37m23m15m9m8m
012m15m26m35m48m49m45m35m24m14m9m5m
114m16m24m34m44m47m44m32m23m14m10m6m
216m17m24m32m42m44m38m29m20m13m10m5m
316m18m25m30m38m39m34m27m18m14m9m7m
419m21m25m29m32m34m32m26m17m11m8m7m
520m21m22m27m31m30m27m22m15m12m8m7m
620m21m23m24m28m26m23m19m16m10m8m6m
722m21m22m24m24m23m20m18m13m10m7m4m
822m23m22m23m21m20m19m14m12m10m7m6m
921m20m19m20m17m19m15m14m12m9m7m5m
1019m18m16m17m16m17m14m13m10m9m6m5m
1118m17m15m14m14m13m12m10m9m7m5m7m
<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

Response time by planWhiskers span the adjacent values — dots past them are outliers
24.4109.5194.5279.6StarterGrowthScaleStarter: min 42, Q1 68, median 84, Q3 102, max 128Starter: min 42, Q1 68, median 84, Q3 102, max 128Growth: min 58, Q1 92, median 118, Q3 148, max 178 (2 outliers: 210, 226)Growth: min 58, Q1 92, median 118, Q3 148, max 178 (2 outliers: 210, 226)Growth: outlier 210Growth: outlier 226Scale: min 96, Q1 148, median 182, Q3 218, max 262Scale: min 96, Q1 148, median 182, Q3 218, max 262
CategoryMinQ1MedianQ3Max
Starter426884102128
Growth5892118148178
Scale96148182218262
<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

Build minutes by runnerRaw samples reduce to Tukey quartiles with 1.5×IQR fences at build time
2.43.74.96.2LinuxmacOSLinux — Node 20: min 3.1, Q1 3.3, median 3.5, Q3 3.8, max 4.1 (1 outlier: 5.9)Linux — Node 20: min 3.1, Q1 3.3, median 3.5, Q3 3.8, max 4.1 (1 outlier: 5.9)Linux — Node 20: outlier 5.9macOS — Node 20: min 4.1, Q1 4.2, median 4.4, Q3 4.5, max 4.8macOS — Node 20: min 4.1, Q1 4.2, median 4.4, Q3 4.5, max 4.8Linux — Node 22: min 2.7, Q1 2.9, median 3, Q3 3.1, max 3.3Linux — Node 22: min 2.7, Q1 2.9, median 3, Q3 3.1, max 3.3macOS — Node 22: min 3.7, Q1 3.9, median 4.0, Q3 4.2, max 4.4macOS — Node 22: min 3.7, Q1 3.9, median 4.0, Q3 4.2, max 4.4
CategoryGroupMinQ1MedianQ3Max
LinuxNode 203.13.33.53.84.1
macOSNode 204.14.24.44.54.8
LinuxNode 222.72.933.13.3
macOSNode 223.73.94.04.24.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

PropTypeDefaultDescription
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.
labelsstring[]requiredCategory labels (x axis; slice names for pie/donut).
seriesChartSeries[]requiredData series: { name, data, color? }.
titlestring—Accessible title, rendered as figcaption.
descriptionstring—Supporting line under the title.
gridbooleantrueGrid lines on cartesian charts.
legendbooleantrueLegend under the chart.
innerRadiusnumber58Donut hole percentage.
padAnglenumber0Gap between pie/donut slices, in degrees.
sliceRadiusnumber0Corner 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.
collapseBelownumber0Merge pie/donut slices below this percent share into a trailing muted Other slice (hover lists the contents).
depthnumber363D pie wall height in px (clamped 0–120) — chunkier extrusions read more dimensional; 0 is a flat tilted ellipse.
tileRadiusnumber10Treemap 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.
binsnumberSturgesHistogram bin count override (1–40); defaults to ceil(log2(n) + 1) over the sample count.
barGapnumber2Histogram gutter between bars in px (never more than half a bar); 0 restores touching bars.
bulletsBulletRow[]—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.
nodesSankeyNode[]—Sankey nodes: { name, color? }.
linksSankeyLink[]—Sankey flows: { source, target, value, color? }.
maxYnumberdata maxY-axis ceiling override (per-label totals max when stacked).
rightMaxYnumberright-axis data maxSecondary-axis ceiling in combo charts.
curveboolean | "step" | "bump"falseSmooth 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.
gradientbooleanfalseGradient fills: area fades toward the baseline, bars per bar, pie/donut rim-strong radial fade.
ticksnumber36Tick 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).
gaugeRampstring[]—2+ CSS colors interpolated across the gauge arc (block style).
gradientIntensitynumber1Multiplier on gradient fade strength (clamped 0.1–2.5) — applies to every gradient type.
stackboolean | "percent"falseStack bar series (cumulative totals) or normalize each column to 100% with "percent".
waterfallTotalsnumber[]—Waterfall: label indices that render as full total bars and reset the running sum.
waterfallColors[string, string, string]success / error / primaryWaterfall colors for [increase, decrease, total] bars.
heatmapRampstring[]muted → primary2+ CSS colors the heatmap cells interpolate across (data min → max).
heatmapLabelsbooleanfalsePrint the formatted value inside each heatmap cell.
cohortSizesnumber[]—Cohort sizes; renders a users sublabel under each row name.
cohortValues"percent" | "value""percent"Cohort cell text: share of cohort, or headcount via cohortSizes.
cohortMaxnumberdata maxCeiling for the cohort color ramp (fractions vs percents disambiguated automatically).
cohortRampstring[]muted → primary2+ 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).
terrainCalloutsbooleantrueHighest/lowest corner titles, rail and drops; false hides them.
terrainCalloutLabels[string, string]Highest / Lowest pointCustom corner-title texts for the extreme-point callouts.
terrainDotColors[string, string, string]muted two-toneDot colors for [low, middle, high] elevation bands (thirds of the range).
mapRegionsMapRegion[]—Map regions: { name, at: [col, row], span?, points?, value? } — rect tiles or freeform polygons.
mapRampstring[]muted → primary2+ CSS colors the map region fills interpolate across (data min → max).
mapLabelsbooleanfalsePrint the formatted value inside each map region.
mapColumnsnumberrect extentGrid column count the tile cells divide — reserve columns for freeform polygon regions so rect tiles don't stretch.
boxplotsBoxplotGroup[]—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.
boxRadiusnumber8Corner rounding of each boxplot box, in px.
boxplotOutliersbooleantrueRender outlier dots past the boxplot whiskers (still listed in tooltips and the data table when hidden).
zoomablebooleanfalseMap: 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.
heightnumber260Plot height in px.
classstring—Merged via cn(); Tailwind classes welcome.
idstring—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.

v0.1.0 — 2026-11-11
added
  • Data display (10): Badge, Avatar, Attachment, Table, Progress, Bubble, Chart, Empty, Tag, Timeline.
fixed
  • Dialog, Lightbox, Sheet, and AlertDialog backdrops, Progress stripes, Carousel shadows, and Chart SVG shadows resolve through tokens instead of hard-coded colors.