@stealthscale/component-charts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1862 @@
1
+ # @stealthscale/component-charts
2
+
3
+ `@stealthscale/component-charts` renders data as charts over recharts. The presets render line, area
4
+ and bar charts from declared series, bars and lines on two value axes, a forecast band, Pareto,
5
+ burndown, stream and waterfall charts, scatter and bubble charts with a trend line or four named
6
+ quadrants, histograms, distributions compared on shared bins, box plots and violins of groups,
7
+ candlesticks of prices, a pie or a donut from slices, a radar of dimensions, measures as rings
8
+ against a full turn, a rose of a cycle, one value on a dial against its zones, the stages of a
9
+ funnel with their rates, a hierarchy of parts as nested tiles or as rings, flows between stages as
10
+ bands, flows between peers as ribbons around a circle, a value for every pair of two sets of
11
+ categories as a grid of colored cells, moments on one time window across lanes, and a run of values
12
+ the size of a word. The presets are built on `Chart`, which renders a figure around recharts' own
13
+ chart with a measured plot, a tooltip, a legend that shows and hides series, a caption and an empty
14
+ state. A line, area or bar chart also marks moments, periods and points on its categories, and plots
15
+ an earlier period behind the current one. The heatmap renders an HTML grid in the figure in place of
16
+ a recharts chart. A caller who needs another chart writes it inside `Chart`. Each series takes its
17
+ color from the theme. The preset under `./theme` registers the recipes with an application's
18
+ compiler.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pnpm add @stealthscale/component-charts recharts react-is
24
+ ```
25
+
26
+ The package depends on `recharts` 3.10.1, pinned exactly, because the recipe restyles the classes
27
+ recharts puts on its SVG. It depends on `@stealthscale/component-data` for `ColorSwatch` and on
28
+ `@stealthscale/component-collections` for the funnel's table and the heatmap's scroll area. It peers
29
+ on `react`, `react-is`, which recharts asks for, `@stealthscale/component-primitives`, whose scroll
30
+ area the table renders in, `@stealthscale/theme`, `@stealthscale/hooks` and
31
+ `@stealthscale/provider-locale`.
32
+
33
+ ## Presets
34
+
35
+ A preset takes the rows, the field the category axis reads and the series, and renders the whole
36
+ chart:
37
+
38
+ ```tsx
39
+ import { BarChart } from "@stealthscale/component-charts";
40
+
41
+ <BarChart
42
+ caption="Growth sign-ups passed Starter in September, 158 to 152."
43
+ categoryKey="month"
44
+ data={months}
45
+ label="Sign-ups per plan per month"
46
+ labelOptions={{ month: "short", timeZone: "UTC" }}
47
+ series={[
48
+ { key: "starter", label: "Starter" },
49
+ { key: "growth", label: "Growth" },
50
+ ]}
51
+ />;
52
+ ```
53
+
54
+ | Preset | Renders | Own props |
55
+ | -------------------- | ---------------------------------------------------------- | ------------------------------------------ |
56
+ | `LineChart` | A line per series | `curve` |
57
+ | `StepLineChart` | A line per series, flat from each point to the next | |
58
+ | `AreaChart` | An area per series, each fading to the axis | `curve` |
59
+ | `StackedAreaChart` | The series stacked into one total | `curve`, `percent` |
60
+ | `BarChart` | A bar per series, side by side in each category | `zones`, `targetLabel`, a series' `target` |
61
+ | `StackedBarChart` | Each category's total as one bar, split into the series | |
62
+ | `PercentStackedBar` | Each category as one bar of 100%, split into shares | |
63
+ | `HorizontalBarChart` | A bar per series on its side, the categories down the edge | `zones`, `targetLabel`, a series' `target` |
64
+
65
+ `curve` is `monotone` unless stated, or `linear` for straight lines between the points. With
66
+ `percent` every point of a `StackedAreaChart` sums to 100%.
67
+
68
+ | Prop | Takes | Default |
69
+ | ------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------- |
70
+ | `data` | The rows, in order | Required |
71
+ | `categoryKey` | The field of each row the category axis reads | Required |
72
+ | `series` | `{ key, label, color, dashed, previousOf }` per series, in legend order | Required |
73
+ | `annotations` | `{ key, at, label, color, until, value }` per mark on the categories | None |
74
+ | `label` | The chart's accessible name, recharts' `title` | Required |
75
+ | `caption` | The finding in words, which names the figure | None |
76
+ | `labelOptions` | `Intl.DateTimeFormatOptions` for the category ticks and the tooltip's head | The category as it is |
77
+ | `valueOptions` | `Intl.NumberFormatOptions` for the value ticks and the tooltip's values | A number in the locale |
78
+ | `valueDomain` | recharts' domain of the value axis, such as `[0.99, 1]` | From zero |
79
+ | `grid` | Whether lines cross the plot at the value ticks | `true` |
80
+ | `legend` | Whether the legend renders | Two or more series, and rows |
81
+ | `legendLabel` | The legend's accessible name | `Series` |
82
+ | `defaultIndex` | The row the tooltip shows on the first render | None |
83
+ | `animate` | Whether the marks animate in, which they never do under reduced motion | `false` |
84
+ | `empty` | The message in the plot's place while the chart has no rows | `No data` |
85
+ | `ratio` | The plot's aspect ratio | `video` |
86
+ | `hiddenKeys`, `defaultHiddenKeys`, `onHiddenKeysChange` | The hidden series, controlled or from a first value | None hidden |
87
+ | `locale` | The locale the values are written in | The provider's, else the runtime's |
88
+ | `children` | recharts elements rendered after the marks, such as a `ReferenceLine` | None |
89
+
90
+ Any other prop goes to the `figure`.
91
+
92
+ ### Marks
93
+
94
+ - A line and an area's edge are 2px wide and have no dots. The point the tooltip is at takes a dot
95
+ of radius 5 inside an edge in the panel's color.
96
+ - A series with `dashed` renders its line in 6px dashes 4px apart, for a target or a projection. A
97
+ series with `previousOf` renders dashed the same way.
98
+ - An unstacked area fills with a gradient from 0.3 opacity at its top to transparent at the axis, so
99
+ overlapping areas show through each other. A stacked area fills flat at 0.85 opacity.
100
+ - An unstacked bar rounds its end by 4px. A stacked bar keeps square ends, and a hairline edge in
101
+ the panel's color parts the segments of a stack.
102
+ - In a stack summed to 100% the value ticks read in percent, and the tooltip writes each series' own
103
+ value, not its share.
104
+ - The value axis labels every tick, and the grid renders a line at every label.
105
+
106
+ ### A reference line
107
+
108
+ A child renders inside the chart after the marks. `Chart.colorOf` returns the chart color of a
109
+ palette for its stroke, and the recipe sets its label in the chart's muted ink:
110
+
111
+ ```tsx
112
+ import { ReferenceLine } from "recharts";
113
+
114
+ import { Chart, LineChart } from "@stealthscale/component-charts";
115
+
116
+ <LineChart categoryKey="day" data={days} label="Cloud spend per day" series={[{ key: "spend" }]}>
117
+ <ReferenceLine
118
+ label={{ position: "insideTopRight", value: "Budget" }}
119
+ stroke={Chart.colorOf("error")}
120
+ strokeDasharray="6 4"
121
+ y={1200}
122
+ />
123
+ </LineChart>;
124
+ ```
125
+
126
+ The recipe's muted ink applies over any `fill` a caller passes to a `Label`.
127
+
128
+ ### Annotations
129
+
130
+ `annotations` marks moments, periods and points on the category axis. The fields of each annotation
131
+ decide its mark:
132
+
133
+ ```tsx
134
+ import { LineChart } from "@stealthscale/component-charts";
135
+
136
+ <LineChart
137
+ annotations={[
138
+ { at: "2026-09-17", key: "4.12", label: "Release 4.12" },
139
+ { at: "2026-09-20", color: "info", key: "freeze", label: "Change freeze", until: "2026-09-22" },
140
+ { at: "2026-09-18", color: "error", key: "outage", label: "Processor outage", value: 312 },
141
+ ]}
142
+ categoryKey="day"
143
+ data={days}
144
+ label="p95 latency per day"
145
+ series={[{ key: "p95", label: "p95 latency" }]}
146
+ />;
147
+ ```
148
+
149
+ | Fields | Mark |
150
+ | ---------------- | ------------------------------------------------------------------------------- |
151
+ | `at` | A moment: a rule across the plot in 4px dashes 3px apart |
152
+ | `at` and `until` | A period: a wash from `at` to `until` at 0.12 opacity, without an edge |
153
+ | `at` and `value` | A point: a ring of radius 5 around `value` at `at`, 2px wide and without a fill |
154
+
155
+ - `at` and `until` are categories of the chart, in the values the category axis reads. A period's
156
+ ends may come in either order.
157
+ - A mark takes its annotation's `color`, the neutral palette's chart color unless stated.
158
+ - recharts renders a wash under the series and a rule and a ring over them. A ring widens the value
159
+ axis to its value. On a chart of bars on their side the marks run along the category axis, down
160
+ the plot.
161
+ - The words start 4px inside the top of a rule or a wash, or centre 4px above a ring, in the label
162
+ ink inside a halo in the panel's color. Words that leave the plot's sides, or meet the words of an
163
+ annotation earlier in the list, are hidden.
164
+ - The tooltip lists the annotations at its category after the series' rows, each with a swatch in
165
+ its color. The arrows move the tooltip through every category, so the live region reads each
166
+ annotation, hidden words included.
167
+
168
+ ### Two periods
169
+
170
+ `alignPeriods(current, previous, options)` returns a row per row of the current period, with the
171
+ fields of the previous period's row at the same position. A series with `previousOf` is the earlier
172
+ period of the series it names:
173
+
174
+ ```tsx
175
+ import { alignPeriods, LineChart } from "@stealthscale/component-charts";
176
+
177
+ const days = alignPeriods(thisWeek, lastWeek, { categoryKey: "day", keys: ["revenue"] });
178
+
179
+ <LineChart
180
+ categoryKey="day"
181
+ data={days}
182
+ label="Revenue per day"
183
+ series={[
184
+ { key: "revenue", label: "This week" },
185
+ { key: "revenuePrevious", label: "Last week", previousOf: "revenue" },
186
+ ]}
187
+ />;
188
+ ```
189
+
190
+ - The periods match by position, the first row against the first row. A previous period that is
191
+ shorter leaves the last rows without its values.
192
+ - Each copied field takes the suffix `Previous` unless `suffix` states another, and the previous
193
+ period's own category is copied too, as `dayPrevious`.
194
+ - A series with `previousOf` renders dashed, in the neutral palette's chart color unless it states a
195
+ `color`.
196
+ - The tooltip writes the change from the earlier period after the current value, as a signed
197
+ percentage to one decimal after the locale's list separator: "€26,300, +8.2%". A row without a
198
+ finite earlier value, or with an earlier value of zero, writes the value alone.
199
+
200
+ ### Zooming a long series
201
+
202
+ A preset plots the rows it receives, so a range picked in another control zooms it. The forms
203
+ `Slider` picks the range with two thumbs over the rows' indices. Each thumb takes its own `label`,
204
+ and `getAriaValueText` writes its date for a screen reader. The arrow keys, Page Up, Page Down, Home
205
+ and End move the focused thumb:
206
+
207
+ ```tsx
208
+ import { useState } from "react";
209
+
210
+ import { AreaChart } from "@stealthscale/component-charts";
211
+ import { Slider } from "@stealthscale/component-forms";
212
+
213
+ const [range, setRange] = useState([days.length - 91, days.length - 1]);
214
+ const [from = 0, to = 0] = range;
215
+
216
+ <>
217
+ <AreaChart
218
+ categoryKey="day"
219
+ data={days.slice(from, to + 1)}
220
+ label="Visitors per day"
221
+ series={[{ key: "visitors", label: "Visitors" }]}
222
+ />
223
+ <Slider.Root
224
+ getAriaValueText={({ value }) => dateOf(value)}
225
+ max={days.length - 1}
226
+ minStepsBetweenThumbs={6}
227
+ onValueChange={({ value }) => {
228
+ setRange(value);
229
+ }}
230
+ value={range}
231
+ >
232
+ <Slider.Label>Days shown</Slider.Label>
233
+ <Slider.Control>
234
+ <Slider.Track>
235
+ <Slider.Range />
236
+ </Slider.Track>
237
+ <Slider.Thumb index={0} label="From" />
238
+ <Slider.Thumb index={1} label="To" />
239
+ </Slider.Control>
240
+ </Slider.Root>
241
+ </>;
242
+ ```
243
+
244
+ ### Targets and zones
245
+
246
+ A bar chart's series with `target` reads each row's target from that field and marks it with a tick
247
+ across the row's bar. `zones` fill each bar's row with the zones of the value axis. With both, each
248
+ bar is a bullet graph, and the rows compare their measures and their targets on one scale.
249
+
250
+ ```tsx
251
+ import { HorizontalBarChart } from "@stealthscale/component-charts";
252
+
253
+ <HorizontalBarChart
254
+ caption="Two of six teams passed their plan, and Borealis is behind at 64%."
255
+ categoryKey="team"
256
+ data={teams}
257
+ label="Plan attainment per team"
258
+ series={[{ key: "done", label: "Attainment", target: "plan" }]}
259
+ targetLabel="Plan"
260
+ valueDomain={[0, 1.2]}
261
+ valueOptions={{ style: "percent" }}
262
+ zones={[
263
+ { color: "error", label: "Behind", upTo: 0.7 },
264
+ { color: "warning", label: "Close", upTo: 0.9 },
265
+ { color: "success", label: "On plan", upTo: 1.2 },
266
+ ]}
267
+ />;
268
+ ```
269
+
270
+ - The tick is 2px wide and 76% of the bar's thickness, in the ink inside a halo in the panel's
271
+ color. A target past either end of the value axis is placed at that end. A row whose target is not
272
+ a finite number renders no tick.
273
+ - `zones` take the gauge's `GaugeZone`: where each zone ends, its palette and its name. They resolve
274
+ against the value axis' ends, which `valueDomain` states, and every row shares them.
275
+ - Each zone fills the whole row, tinted at 40% of its palette over the panel, and the part of the
276
+ axis no zone covers takes the track's fill. With zones the measure is 42% of the row's thickness,
277
+ centred.
278
+ - The tooltip writes the value, then the target after `targetLabel`, "Target" unless stated, then
279
+ the name of the zone the value is in, where the name is text.
280
+ - A key under the plot labels the tick with `targetLabel` and lists each zone with a `label`.
281
+ - The zones and the tick keep their places while the bars animate in.
282
+
283
+ ### Keys
284
+
285
+ recharts' keyboard layer inverts the arrows in a sideways layout. ArrowLeft moves the tooltip down a
286
+ `HorizontalBarChart`, and ArrowRight moves it up.
287
+
288
+ ## Mixed marks
289
+
290
+ `ComboChart`, `RangeChart`, `ParetoChart` and `BurndownChart` render marks of different kinds on one
291
+ plot. They take the preset props, and each adds its own:
292
+
293
+ ```tsx
294
+ import { ComboChart } from "@stealthscale/component-charts";
295
+
296
+ <ComboChart
297
+ caption="Revenue rose to €254K in September at a 62% gross margin."
298
+ categoryKey="month"
299
+ data={months}
300
+ endDomain={[0, 1]}
301
+ endOptions={{ style: "percent" }}
302
+ label="Revenue and gross margin per month"
303
+ series={[
304
+ { key: "revenue", label: "Revenue", mark: "bar" },
305
+ { axis: "end", key: "margin", label: "Gross margin", mark: "line" },
306
+ ]}
307
+ />;
308
+ ```
309
+
310
+ | Preset | Renders | Own props |
311
+ | --------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
312
+ | `ComboChart` | Each series as the mark it states: `bar`, `line` or `area` | `curve`, `endDomain`, `endOptions` |
313
+ | `RangeChart` | A band between two fields of each row, and lines plotted over it | `band`, `curve` |
314
+ | `ParetoChart` | Bars sorted largest first, and their running share on an end axis | `valueKey`, `valueLabel`, `cumulativeLabel`, `threshold`, `thresholdLabel` |
315
+ | `BurndownChart` | The work left per period, the ideal line and the projection | `points`, `periods`, `total`, `projection`, `remainingLabel`, `idealLabel`, `projectedLabel` |
316
+
317
+ - A combo series takes `mark` and `axis`. `axis: "end"` puts a series on a second value axis at the
318
+ plot's end edge, which renders while a series reads it. `endOptions` writes its ticks and its
319
+ series' tooltip values, and `endDomain` sets its range, from zero unless stated.
320
+ - recharts renders areas behind bars and bars behind lines, whatever the series' order. A combo
321
+ chart's cursor is a line, because only recharts' `BarChart` renders the band cursor.
322
+ - A range chart's `band` takes `{ key, label, color, low, high }`. It fills the space between its
323
+ two fields at 0.2 opacity with no edge, and the tooltip writes it as a range, such as
324
+ "15,500–19,500". The low field must be below the high field in every row.
325
+ - A Pareto chart sorts the rows largest first. `paretoRows` returns the sorted rows with each row's
326
+ `share` and running `cumulative` share, and `paretoCutoff(rows, threshold)` counts the rows it
327
+ takes to add up to the threshold, so a caption can state the count. The dashed threshold line is
328
+ at 80% unless `threshold` states otherwise, and `0` renders none.
329
+ - A burndown chart takes `{ at, remaining }` per period, with `remaining` missing for a period to
330
+ come. The ideal line runs straight from `total`, else the first reading, to zero over `periods`,
331
+ else over the points. The projection continues the least-squares trend of the readings from the
332
+ last reading and never goes below zero. A trend that does not fall has no projection.
333
+ - `burndownFinish` returns the period the projection falls to zero, as a fractional index, and
334
+ `burndownRows` returns the rows the chart plots. The ideal line and the projection take fractional
335
+ values. The burndown writes its values to one decimal unless `valueOptions` states otherwise.
336
+
337
+ ## Stream and waterfall
338
+
339
+ `StreamGraph` stacks many series about a moving baseline. Each band's rise and fall reads across a
340
+ long span:
341
+
342
+ - `baseline` is `wiggle` unless stated, or `silhouette`, which centres the stack on a straight line.
343
+ The graph renders no value axis and no grid, and the stack fills the plot's height.
344
+ - The bands stack inside out. The series are sorted by onset, the centre of each series' mass, and
345
+ dealt onto the lighter of two piles, so a series that arrives late is outside and moves no band
346
+ beneath it. `insideOut={false}` keeps the series' order, the first at the bottom.
347
+ - The stack's order changes neither a series' color nor its place in the legend and the tooltip.
348
+ `insideOutOrder` and `streamOnset` return the order and the onsets.
349
+ - The legend renders while the series fit the theme's eight series colors.
350
+
351
+ `WaterfallChart` renders the steps that take a running total from one number to another:
352
+
353
+ ```tsx
354
+ import { WaterfallChart } from "@stealthscale/component-charts";
355
+
356
+ <WaterfallChart
357
+ caption="MRR grew from €120K in August to €135.9K in September."
358
+ label="MRR bridge from August to September"
359
+ steps={[
360
+ { key: "august", label: "August", total: true, value: 120_000 },
361
+ { key: "new", label: "New", value: 18_400 },
362
+ { key: "churn", label: "Churn", value: -7600 },
363
+ { key: "september", label: "September", total: true },
364
+ ]}
365
+ valueOptions={{ currency: "EUR", notation: "compact", style: "currency" }}
366
+ />;
367
+ ```
368
+
369
+ - A change floats from the running total before it to the total after it, in the success palette's
370
+ chart color when it rises and the error palette's when it falls. A total rises from zero in the
371
+ neutral palette's chart color.
372
+ - A total with a `value` sets the running total, for an opening balance. A total without one shows
373
+ the running total.
374
+ - A dashed connector joins each bar to the next at the running total. The chart writes each bar's
375
+ signed change or total above the bar and keeps 20px free above the tallest bar for it.
376
+ - The values show while the widest one fits a step, at 8px a character, and hide together in a
377
+ narrower plot.
378
+ - The tooltip writes the same value as the bar, and its swatch takes the bar's color.
379
+ - The value axis rounds its range to whole ticks and runs through zero where the running total goes
380
+ below it. `waterfallBars` returns the bars the chart renders, with each step's `start`, `end` and
381
+ `change`.
382
+
383
+ | Prop | Takes | Default |
384
+ | ------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------- |
385
+ | `steps` | `{ key, label, value, total }` per step, in the order they happened | Required |
386
+ | `label` | The chart's accessible name, recharts' `title` | Required |
387
+ | `caption` | The finding in words, which names the figure | None |
388
+ | `valueLabel` | The name of the values in the tooltip | `Amount` |
389
+ | `valueLabels` | Whether each bar's value is written above it | `true` |
390
+ | `connectors` | Whether dashed connectors join the bars | `true` |
391
+ | `valueOptions` | `Intl.NumberFormatOptions` for the ticks, the tooltip and the bars | A number |
392
+ | `grid`, `defaultIndex`, `animate`, `empty`, `locale`, `ratio`, `children` | As the cartesian presets take them | |
393
+
394
+ ## Points
395
+
396
+ `ScatterPlot` renders a point per thing at two measures, and `BubbleChart` also sizes each point by
397
+ a third:
398
+
399
+ ```tsx
400
+ import { ScatterPlot } from "@stealthscale/component-charts";
401
+
402
+ <ScatterPlot
403
+ caption="Mid-market deals close within 50 days, while enterprise deals take 88 to 178 days."
404
+ label="Days to close by deal size"
405
+ series={[
406
+ { key: "mid", label: "Mid-market", points: midMarket },
407
+ { key: "enterprise", label: "Enterprise", points: enterprise },
408
+ ]}
409
+ xKey="size"
410
+ xLabel="Deal size"
411
+ yKey="days"
412
+ yLabel="Days to close"
413
+ />;
414
+ ```
415
+
416
+ - Each series takes its own `points`, because a scatter's series do not share a category. `xKey` and
417
+ `yKey` are the fields of a point.
418
+ - `xLabel` and `yLabel` are required titles, because neither axis of a scatter explains itself. The
419
+ x title renders under the ticks and the y title along the start edge. `xOptions`, `yOptions`,
420
+ `xDomain` and `yDomain` format and range each axis.
421
+ - A point is 60 square pixels unless `size` states otherwise. `BubbleChart` takes `sizeKey` and
422
+ `sizeLabel`, and recharts scales each point's area over `sizeRange`, 40 to 1600 square pixels
423
+ unless stated. Both axes are padded by the largest point's radius, so no point crosses the plot's
424
+ edge.
425
+ - The tooltip's heading is the label of the point's series. Each row writes a value in its axis'
426
+ format beside the axis' title. `defaultIndex` opens it at a point of the first series.
427
+ - recharts' keyboard layer walks the first series' points only, measured in Firefox and Chromium, so
428
+ the series a reader walks goes first.
429
+ - `RegressionOverlay` is a recharts child that renders the least-squares line through `data` as a
430
+ dashed segment over the x range the points cover. The fit leaves out a point whose x or y is not a
431
+ finite number. `linearRegression(points, xKey, yKey)` returns the line's `slope`, `intercept` and
432
+ `r2`. A caption states r², and a page leaves the line out where r² is low.
433
+ - With `series`, the line takes the series' color, hides while the legend hides the series, and
434
+ fades with the series' points. Without it, the line takes the neutral palette's chart color and
435
+ ignores the legend, for a fit across series.
436
+ - recharts' own fitted line, `lineType="fitting"` on a `Scatter`, fits a point without a value at
437
+ pixel (0, 0), the top-left corner of the chart's `svg`, and returns no r².
438
+
439
+ ### Quadrants
440
+
441
+ A quadrant chart divides the plot into four named quadrants and writes each point's name beside it:
442
+
443
+ ```tsx
444
+ import { ScatterPlot } from "@stealthscale/component-charts";
445
+
446
+ <ScatterPlot
447
+ caption="Alder and Cedar lead on both vision and execution."
448
+ label="Vendors by vision and execution"
449
+ labelKey="name"
450
+ quadrants={{
451
+ names: {
452
+ bottomEnd: "Visionaries",
453
+ bottomStart: "Niche players",
454
+ topEnd: "Leaders",
455
+ topStart: "Challengers",
456
+ },
457
+ }}
458
+ series={[{ key: "vendors", label: "Vendors", points: vendors }]}
459
+ xEnds={["Low", "High"]}
460
+ xKey="vision"
461
+ xLabel="Vision"
462
+ yEnds={["Low", "High"]}
463
+ yKey="execution"
464
+ yLabel="Execution"
465
+ />;
466
+ ```
467
+
468
+ - `quadrants` takes the name of each quadrant, keyed `topStart`, `topEnd`, `bottomStart` and
469
+ `bottomEnd` in the reading direction, and `x` and `y`, the values the plot divides at. Unless they
470
+ are stated, the plot divides at the middle of each axis' span: the axis' domain where it states
471
+ two numbers, 0 to 1 for an axis with named ends, else the points' own values.
472
+ - Two dashed lines in the emphasized border ink divide the plot, and each name is written in its
473
+ corner in the muted ink. A point on a line belongs to the upper or the end side, and
474
+ `quadrantOf(x, y, division)` returns a point's quadrant.
475
+ - `labelKey` names a field of each point whose words are written beside the point and head its
476
+ tooltip. The words run toward the middle past 72% of the plot's width. Words that leave the plot,
477
+ or meet the words of a point earlier in the series, are hidden, and the tooltip still names the
478
+ point.
479
+ - With `quadrants`, the tooltip's heading names the point's quadrant after the point, joined by the
480
+ locale's list separator: "Alder, Leaders".
481
+ - `xEnds` and `yEnds` name an axis' two ends, such as "Rare" and "Certain", in place of its ticks.
482
+ The axis spans `xDomain` or `yDomain`, 0 to 1 unless it states two numbers, and the grid renders
483
+ the plot's two edges alone along it.
484
+ - `spreadPoints(points, options)` moves points closer than `distance` apart, 0.06 of each axis' span
485
+ unless stated, along a golden-angle spiral around each point's place. Each point keeps its
486
+ quadrant, and every call places the points the same way. The function moves the plotted values, so
487
+ the tooltip writes the moved values: it suits scores read by quadrant, never measurements.
488
+
489
+ ## Distributions
490
+
491
+ `HistogramChart` renders the shape of one distribution: its values counted into bins that touch
492
+ along a numeric axis.
493
+
494
+ ```tsx
495
+ import { HistogramChart } from "@stealthscale/component-charts";
496
+
497
+ <HistogramChart
498
+ caption="107 of 140 requests finish under 60ms, and a slow tail runs to 460ms."
499
+ countLabel="Requests"
500
+ label="Response times of the checkout API"
501
+ valueOptions={{ style: "unit", unit: "millisecond", unitDisplay: "narrow" }}
502
+ values={latencies}
503
+ />;
504
+ ```
505
+
506
+ - The chart takes `values`, one number per thing measured, not rows. A value that is not a finite
507
+ number is left out.
508
+ - `binCount(values)` returns Freedman–Diaconis' count, or Sturges' where the middle half of the
509
+ values ties, at most 30. `binValues(values, { bins, domain })` counts the values into bins whose
510
+ width snaps to the nearest of 1, 2 or 5 times a power of ten, as d3's and Vega's bins do, and one
511
+ step coarser where the bins would pass 30.
512
+ - `bins` aims for a number of bins. `domain` bins a range from its lower edge and leaves out the
513
+ values outside the bins. When every value is equal, the chart counts them in one bin a tenth of
514
+ the value wide.
515
+ - A bin runs from its lower edge up to its upper edge and leaves the upper edge to the next bin. The
516
+ last bin includes its upper edge, so the largest value is counted.
517
+ - The bars touch, parted by the recipe's hairline in the panel's color. Each bar runs from its lower
518
+ edge to its upper edge at the axis' own pixels, where recharts rounds a bar's width to a whole
519
+ pixel.
520
+ - The axis labels every edge, else every second or every fifth, so at most eight even steps show.
521
+ The tooltip heads each bar with its range, such as "20–40ms", and writes its count.
522
+
523
+ | Prop | Takes | Default |
524
+ | ------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------ |
525
+ | `values` | The values to count, one per thing measured | Required |
526
+ | `label` | The chart's accessible name, recharts' `title` | Required |
527
+ | `caption` | The finding in words, which names the figure | None |
528
+ | `bins` | The number of bins to aim for, at most 30 | Freedman–Diaconis' count |
529
+ | `domain` | The range to bin, from its lower edge | The values' range |
530
+ | `countLabel` | The name of the counts in the tooltip | `Count` |
531
+ | `color` | A hue or a palette for the bars | `series.1` |
532
+ | `valueOptions` | `Intl.NumberFormatOptions` for the edges and the tooltip's heading | A number in the locale |
533
+ | `grid`, `defaultIndex`, `animate`, `empty`, `locale`, `ratio`, `children` | As the cartesian presets take them | |
534
+
535
+ `DistributionChart` compares two or more distributions on one set of bins:
536
+
537
+ ```tsx
538
+ import { DistributionChart } from "@stealthscale/component-charts";
539
+
540
+ <DistributionChart
541
+ caption="Three in four Free sessions end within 10 minutes, against one in ten Pro sessions."
542
+ label="Session lengths by plan"
543
+ normalize
544
+ series={[
545
+ { key: "free", label: "Free", values: freeSessions },
546
+ { key: "pro", label: "Pro", values: proSessions },
547
+ ]}
548
+ valueOptions={{ style: "unit", unit: "minute", unitDisplay: "short" }}
549
+ />;
550
+ ```
551
+
552
+ - The chart chooses the bins from every series' values together, so the series' bars start at the
553
+ same edges. Histograms binned apart put their bars at different edges and widths.
554
+ - `normalize` counts each bin as a share of its own series' total, so series of different sizes
555
+ compare. The value axis and the tooltip then write percents.
556
+ - The bars of a bin are side by side in the series' order, each a share of the bin's width. A series
557
+ the legend hides gives its share to the series shown.
558
+ - A series takes `{ key, label, color, values }`. The chart takes the histogram's `bins`,
559
+ `valueOptions`, `grid`, `defaultIndex`, `animate`, `empty`, `locale`, `ratio` and `children`, and
560
+ the cartesian presets' legend props.
561
+
562
+ `BoxPlot` compares the distributions of groups: each group's median, middle half, whiskers and
563
+ outliers.
564
+
565
+ ```tsx
566
+ import { BoxPlot } from "@stealthscale/component-charts";
567
+
568
+ <BoxPlot
569
+ caption="Singapore's median of 165ms is more than twice Frankfurt's 61ms."
570
+ countLabel="Requests"
571
+ groups={[
572
+ { key: "frankfurt", label: "Frankfurt", values: frankfurt },
573
+ { key: "singapore", label: "Singapore", values: singapore },
574
+ ]}
575
+ label="Response times of the checkout API by region"
576
+ valueOptions={{ style: "unit", unit: "millisecond", unitDisplay: "narrow" }}
577
+ />;
578
+ ```
579
+
580
+ - A group takes `{ key, label, values }`, or a `summary` in place of its values, for values
581
+ summarised elsewhere. `boxStats(values, whisker)` returns the summary the chart renders.
582
+ `quantile(sorted, share)` reads its quartiles by type 7, the rule of SQL's `PERCENTILE_CONT`, so a
583
+ query that summarises the values matches the chart.
584
+ - The box runs from the first quartile to the third, and the median crosses it in the ink. Each
585
+ whisker extends to the furthest value within `whisker` interquartile ranges of the box, and ends
586
+ at the box's edge when no value is within that distance.
587
+ - Every value past a whisker renders as a hollow point in the box's color, and outliers of equal
588
+ value share one point. `outliers={false}` leaves the points out.
589
+ - The value axis is rounded around the values, because a box plot compares where values fall.
590
+ `valueDomain` sets another range, such as one scale for every week.
591
+ - A box is at most 72px wide. The box under the pointer or the keyboard fills at 0.6 of its color
592
+ inside an edge 2px wide, and the rest fill at 0.35.
593
+ - The key under the plot labels the box, the median, the whiskers and the outliers beside a glyph of
594
+ each in the chart's inks, because a box plot is read by convention. `legend={false}` leaves it
595
+ out.
596
+ - The tooltip heads a group with its name and writes its median, middle half, whiskers, outliers and
597
+ count. `animate` grows each box from the axis with the bar's entrance.
598
+
599
+ | Prop | Takes | Default |
600
+ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------- |
601
+ | `groups` | `{ key, label, values, summary }` per group, in the axis' order | Required |
602
+ | `label` | The chart's accessible name, recharts' `title` | Required |
603
+ | `caption` | The finding in words, which names the figure | None |
604
+ | `whisker` | The whiskers' reach in interquartile ranges | `1.5` |
605
+ | `outliers` | Whether each outlier renders as a point | `true` |
606
+ | `legend` | Whether the key renders | `true`, while a group has values |
607
+ | `color` | A hue or a palette for the boxes | `series.1` |
608
+ | `valueDomain` | recharts' domain of the value axis | Rounded around the values |
609
+ | `valueOptions` | `Intl.NumberFormatOptions` for the ticks and the tooltip's values | A number in the locale |
610
+ | `medianLabel`, `quartilesLabel`, `whiskersLabel`, `outliersLabel`, `countLabel` | The names in the tooltip and the key | `Median`, `Middle half`, `Whiskers`, `Outliers`, `Count` |
611
+ | `grid`, `defaultIndex`, `animate`, `empty`, `locale`, `ratio`, `children` | As the cartesian presets take them | |
612
+
613
+ `ViolinPlot` renders the density of each group's values, mirrored about the middle of its band, with
614
+ its quartiles inside.
615
+
616
+ ```tsx
617
+ import { ViolinPlot } from "@stealthscale/component-charts";
618
+
619
+ <ViolinPlot
620
+ caption="Search requests peak at 22ms and at 167ms."
621
+ countLabel="Requests"
622
+ groups={[
623
+ { key: "search", label: "Search", values: search },
624
+ { key: "checkout", label: "Checkout", values: checkout },
625
+ ]}
626
+ label="Response times of the shop's API by endpoint"
627
+ valueOptions={{ style: "unit", unit: "millisecond", unitDisplay: "narrow" }}
628
+ />;
629
+ ```
630
+
631
+ - A group takes `{ key, label, values }`. The chart takes the values themselves, because no summary
632
+ gives a density back.
633
+ - `kernelDensity(values, { bandwidth, resolution })` returns a Gaussian kernel density sampled at
634
+ `resolution` even steps, 64 unless stated, from the smallest value to the largest. The outline
635
+ stops at those values, because a kernel puts mass past the data, such as below zero for a
636
+ duration.
637
+ - Each group's kernel is as wide as `silvermanBandwidth(values)` returns, unless `bandwidth` states
638
+ one width for every group. Silverman's rule takes 0.9 times the smaller of the standard deviation
639
+ and the interquartile range over 1.34, times the count to the power of −1/5.
640
+ - A kernel wider than the gap between two peaks merges them, and a narrow one shows sampling noise
641
+ as peaks.
642
+ - Each violin is as wide as its bar, 110px at most, at its own densest value. Its width compares
643
+ within the group only.
644
+ - Inside each violin, a line runs between the whiskers `boxStats` returns, a narrow bar in the ink
645
+ spans the middle half, and a hollow point marks the median. `quartiles={false}` leaves the three
646
+ out.
647
+ - The violin under the pointer or the keyboard fills at 0.5 of its color inside an edge 2px wide,
648
+ and the rest fill at 0.28.
649
+ - `densityPeaks(density)` returns the values a density peaks at, each at least 5% of the tallest
650
+ peak's height.
651
+ - A violin's tooltip lists its median, middle half, range, peaks and count. Each peak takes the
652
+ decimals of the density's sampling step, because a peak is one of the sampled values.
653
+ - The key labels the density. While `quartiles` is on, it also labels the middle half and the
654
+ median.
655
+
656
+ | Prop | Takes | Default |
657
+ | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------- |
658
+ | `groups` | `{ key, label, values }` per group, in the axis' order | Required |
659
+ | `label` | The chart's accessible name, recharts' `title` | Required |
660
+ | `caption` | The finding in words, which names the figure | None |
661
+ | `bandwidth` | The kernel's standard deviation for every group, in the values' units | Silverman's, per group |
662
+ | `resolution` | The number of points each density is sampled at | `64` |
663
+ | `quartiles` | Whether the line, the bar and the point render inside each violin | `true` |
664
+ | `legend` | Whether the key renders | `true`, while a group has values |
665
+ | `color`, `valueDomain`, `valueOptions` | As the box plot takes them | |
666
+ | `medianLabel`, `quartilesLabel`, `rangeLabel`, `peaksLabel`, `countLabel`, `densityLabel` | The names in the tooltip and the key | `Median`, `Middle half`, `Range`, `Peaks`, `Count`, `Density` |
667
+ | `grid`, `defaultIndex`, `animate`, `empty`, `locale`, `ratio`, `children` | As the cartesian presets take them | |
668
+
669
+ `CandlestickChart` renders each period's open, high, low and close as a body inside a wick:
670
+
671
+ ```tsx
672
+ import { CandlestickChart } from "@stealthscale/component-charts";
673
+
674
+ <CandlestickChart
675
+ caption="ACME rose 12.7% over September, from $156.22 to $176.03."
676
+ categoryKey="day"
677
+ data={days}
678
+ label="Daily prices of ACME"
679
+ labelOptions={{ day: "numeric", month: "short", timeZone: "UTC" }}
680
+ valueOptions={{ currency: "USD", style: "currency" }}
681
+ />;
682
+ ```
683
+
684
+ - Each row takes `open`, `high`, `low` and `close`. A row without four finite prices is left out.
685
+ - The body runs from the open to the close, and the wick from the low to the high.
686
+ - `candleDirection(candle)` compares the close with the candle's own open. A rising candle takes the
687
+ success palette's chart color, a falling one the error palette's, and a flat one the neutral
688
+ palette's.
689
+ - `candleChange(candle)` returns the change and its share of the open. The share is `undefined` for
690
+ an open of 0.
691
+ - A doji, a period that closed at its open, has a body 1.5px high. A candle is at most 28px wide,
692
+ and the candle under the pointer or the keyboard takes a wick and an edge 2px wide.
693
+ - The category axis is ordinal, so a weekend takes no room. `labelOptions` formats the periods on
694
+ the ticks and in the tooltip's heading.
695
+ - The value axis is rounded around the prices and does not start at zero, because a price is read
696
+ for its movement. Its ticks step by 1, 2, 2.5 or 5 times a power of ten, recharts' `snap125` rule.
697
+ - A candle's tooltip lists its open, high, low and close, then the change with its sign and its
698
+ percentage of the open to two decimals.
699
+ - The key labels the rising color and the falling color with the data package's `ColorSwatch`. It
700
+ labels the flat color while the chart has a flat candle, and then the body and the wick.
701
+
702
+ | Prop | Takes | Default |
703
+ | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------- |
704
+ | `data` | The rows, each with `open`, `high`, `low` and `close` | Required |
705
+ | `categoryKey` | The field of each row the category axis reads | Required |
706
+ | `label` | The chart's accessible name, recharts' `title` | Required |
707
+ | `caption` | The finding in words, which names the figure | None |
708
+ | `labelOptions` | `Intl.DateTimeFormatOptions` for the category ticks and the tooltip's heading | The category as it is |
709
+ | `valueOptions` | `Intl.NumberFormatOptions` for the ticks, the prices and the change | A number in the locale |
710
+ | `valueDomain` | recharts' domain of the value axis | Rounded around the prices |
711
+ | `legend` | Whether the key renders | `true`, while a row has prices |
712
+ | `openLabel`, `highLabel`, `lowLabel`, `closeLabel`, `changeLabel` | The names in the tooltip | `Open`, `High`, `Low`, `Close`, `Change` |
713
+ | `upLabel`, `downLabel`, `flatLabel`, `bodyLabel`, `wickLabel` | The names in the key | `Up`, `Down`, `Flat`, `Open to close`, `Low to high` |
714
+ | `grid`, `defaultIndex`, `animate`, `empty`, `locale`, `ratio`, `children` | As the cartesian presets take them | |
715
+
716
+ ## Pie and donut
717
+
718
+ `PieChart` and `DonutChart` render the parts of one whole from slices:
719
+
720
+ ```tsx
721
+ import { DonutChart } from "@stealthscale/component-charts";
722
+
723
+ <DonutChart
724
+ caption="Video takes 60% of the 195.5 GB in use."
725
+ center="195.5 GB"
726
+ centerLabel="in use"
727
+ label="Storage per kind of file"
728
+ slices={[
729
+ { key: "video", label: "Video", value: 118 },
730
+ { key: "images", label: "Images", value: 42 },
731
+ { key: "archives", label: "Archives", value: 26 },
732
+ ]}
733
+ />;
734
+ ```
735
+
736
+ - The chart renders the slices largest first, from 12 o'clock and clockwise, because a reader
737
+ compares each slice with its neighbours.
738
+ - The chart writes each slice's share of the slices shown on the slice, in the chart's locale, in
739
+ the ink inside a halo in the panel's color, which reads on a slice of any color. The pointer
740
+ passes through a share to its slice. `shares={false}` leaves the shares out.
741
+ - `maxSlices` gathers the slices past it into one slice named by `otherLabel`, keyed `other`, in the
742
+ neutral palette's chart color.
743
+ - A slice the legend hides leaves the pie, and the shares are of the slices shown. The pointer or
744
+ focus on a legend button fades every other slice.
745
+ - `DonutChart` cuts a hole of 58% of the chart's radius and renders `center` and `centerLabel` in
746
+ it. The chart does not sum the slices, because the figure in the hole is as often the largest
747
+ share or a change as the total.
748
+
749
+ | Prop | Takes | Default |
750
+ | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -------------------------- |
751
+ | `slices` | `{ key, label, value, color }` per slice, in any order | Required |
752
+ | `label` | The chart's accessible name, recharts' `title` | Required |
753
+ | `caption` | The finding in words, which names the figure | None |
754
+ | `shares` | Whether each slice's share is written on it | `true` |
755
+ | `maxSlices` | The largest number of slices the pie renders | Every slice |
756
+ | `otherLabel` | The name of the slice the tail gathers into | `Other` |
757
+ | `valueOptions` | `Intl.NumberFormatOptions` for the tooltip's values | A number in the locale |
758
+ | `legend` | Whether the legend renders | `true`, while slices exist |
759
+ | `ratio` | The plot's aspect ratio | `square` |
760
+ | `legendLabel`, `defaultIndex`, `animate`, `empty`, `hiddenKeys`, `defaultHiddenKeys`, `onHiddenKeysChange`, `locale`, `children` | As the cartesian presets take them | |
761
+ | `center`, `centerLabel` | `DonutChart` only: the figure in the hole and its words | None |
762
+
763
+ ## Radial charts
764
+
765
+ `RadarChart` renders each series' values across three or more dimensions as a polygon on one spoke
766
+ per dimension:
767
+
768
+ ```tsx
769
+ import { RadarChart } from "@stealthscale/component-charts";
770
+
771
+ <RadarChart
772
+ caption="Cost is the only score that fell this quarter, from 6 to 4."
773
+ categoryKey="dimension"
774
+ data={scores}
775
+ label="Checkout service scores out of 10"
776
+ series={[
777
+ { key: "current", label: "This quarter" },
778
+ { key: "previous", label: "Last quarter" },
779
+ ]}
780
+ valueDomain={[0, 10]}
781
+ />;
782
+ ```
783
+
784
+ - Each row is a spoke named by `categoryKey`, and each series is a polygon. The chart takes the
785
+ cartesian presets' props.
786
+ - The spokes share one radius axis, so the dimensions must share a scale. `valueDomain` states it,
787
+ such as `[0, 10]` for scores out of ten. recharts rounds the axis from zero around the values
788
+ unless it is stated.
789
+ - The web's rings follow six radius ticks, which step a top of 1, 5, 10 or 100 evenly. `scale`
790
+ writes the ticks up the spoke at 12 o'clock, each hanging from its ring inside a halo in the
791
+ panel's color.
792
+ - A polygon fills at 0.25 of its color inside an edge 2px wide. `filled: false` on a series leaves
793
+ the outline, for three series or more.
794
+ - The web's radius is 72% of the largest circle the plot fits, which leaves the spokes' names room
795
+ beside it. The plot's ratio is `landscape` unless stated.
796
+ - The polygon's shape follows the order of the rows, so two orders of the same scores render two
797
+ shapes. Compare radars only with their spokes in one order.
798
+ - The tooltip's heading is the spoke's name, with a row per series. The arrows walk the spokes, and
799
+ the cursor is a line along the spoke under the pointer.
800
+
801
+ | Prop | Takes | Default |
802
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------- |
803
+ | `series` | `{ key, label, color, filled }` per series, in the legend's order | Required |
804
+ | `scale` | Whether the radius ticks are written up the top spoke | `false` |
805
+ | `ratio` | The plot's aspect ratio | `landscape` |
806
+ | `data`, `categoryKey`, `label`, `caption`, `labelOptions`, `valueOptions`, `valueDomain`, `grid`, `legend`, `legendLabel`, `defaultIndex`, `animate`, `empty`, the hidden keys, `locale`, `children` | As the cartesian presets take them | |
807
+
808
+ `RadialBarChart` renders each measure as a ring read against the value of a full turn, the first
809
+ ring outermost:
810
+
811
+ ```tsx
812
+ import { RadialBarChart } from "@stealthscale/component-charts";
813
+
814
+ <RadialBarChart
815
+ bars={[
816
+ { key: "storage", label: "Storage", value: 0.82 },
817
+ { key: "seats", label: "Seats", value: 0.61 },
818
+ { key: "api", label: "API calls", value: 0.34 },
819
+ ]}
820
+ caption="Storage is at 82% of the plan's quota, and nothing else is past two thirds."
821
+ label="Plan usage"
822
+ max={1}
823
+ valueOptions={{ style: "percent" }}
824
+ />;
825
+ ```
826
+
827
+ - A ring shows one measure against its own track, such as the use of a quota. A bar chart compares
828
+ measures with each other.
829
+ - `max` is the value of a full turn, such as a quota, a target or a capacity. Without it the largest
830
+ value shown closes its track, and every other ring is measured against that value.
831
+ - A ring past `max` closes its track, and the other rings keep their share of `max`. A ring below
832
+ zero renders no arc. The legend and the tooltip write each ring's own value.
833
+ - A ring's length is its angle times its radius, so the same value renders a longer arc further out.
834
+ The legend writes each ring's value beside its name, because an arc is not read for a quantity.
835
+ `values={false}` leaves the names alone.
836
+ - Each ring's track is the rest of its full turn in the neutral palette's `subtle` fill.
837
+ `track={false}` leaves the tracks out. Under forced colors a track fills with `Canvas` inside a
838
+ `GrayText` edge.
839
+ - The rings turn clockwise from 12 o'clock. `startAngle` and `endAngle` take recharts' degrees,
840
+ where 90 is 12 o'clock and clockwise is negative, so `225` and `-45` turn three quarters with the
841
+ gap at the bottom.
842
+ - The hole in the middle is 30% of the largest circle the plot fits. The plot's ratio is `square`
843
+ unless stated.
844
+ - The tooltip writes the ring's name and value without a heading, and no cursor renders. The arrows
845
+ walk the rings from the outermost.
846
+ - A ring the legend hides leaves the chart. With `max` the other rings keep their turn.
847
+
848
+ | Prop | Takes | Default |
849
+ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------ |
850
+ | `bars` | `{ key, label, value, color }` per ring, the outermost first | Required |
851
+ | `label` | The chart's accessible name, recharts' `title` | Required |
852
+ | `caption` | The finding in words, which names the figure | None |
853
+ | `max` | The value of a full turn | The largest value shown |
854
+ | `track` | Whether the rest of each ring's turn renders behind it | `true` |
855
+ | `values` | Whether the legend writes each ring's value beside its name | `true` |
856
+ | `valueOptions` | `Intl.NumberFormatOptions` for the legend's and the tooltip's values | A number in the locale |
857
+ | `startAngle`, `endAngle` | The angles the rings start and end at, in recharts' degrees | `90`, `-270` |
858
+ | `legend` | Whether the legend renders | `true`, while bars exist |
859
+ | `ratio` | The plot's aspect ratio | `square` |
860
+ | `legendLabel`, `defaultIndex`, `animate`, `empty`, the hidden keys, `locale`, `children` | As the cartesian presets take them | |
861
+
862
+ `PolarAreaChart` renders a rose: each category of a cycle as a wedge of the same angle, whose area
863
+ follows its value:
864
+
865
+ ```tsx
866
+ import { PolarAreaChart } from "@stealthscale/component-charts";
867
+
868
+ <PolarAreaChart
869
+ caption="The load builds from 06:00, peaks at noon and falls by 21:00."
870
+ label="Requests a minute by hour"
871
+ slices={[
872
+ { key: "00", label: "00:00", value: 120 },
873
+ { key: "06", label: "06:00", value: 210 },
874
+ { key: "12", label: "12:00", value: 1180 },
875
+ { key: "18", label: "18:00", value: 640 },
876
+ ]}
877
+ />;
878
+ ```
879
+
880
+ - The wedges run clockwise from 12 o'clock in the slices' order, so twelve months read like a clock
881
+ face. A rose suits categories that wrap, such as hours, months or compass points. A bar chart
882
+ compares values on their own.
883
+ - A wedge's radius is √(value / `max`) of the full radius, so its area follows its value. A wedge
884
+ worth four times another is twice as long and covers four times the area, where a radius in
885
+ proportion to the value would cover sixteen times. `areaRadius(value, max)` returns the share of
886
+ the full radius.
887
+ - `max` is the value of a full radius, such as a capacity. Without it the wedge of the largest value
888
+ shown extends to the full radius. A wedge past `max` extends to the full radius too, and a value
889
+ below zero renders no wedge.
890
+ - Every wedge takes `color`, the theme's first series color unless stated, or the palette its slice
891
+ states. The fill rises from 0.45 of the color at zero to the whole color at `max`, so the shade
892
+ repeats the radius.
893
+ - The chart writes the names on one circle around the rose, each centred on its wedge. Names of five
894
+ characters, such as 00:00, clear a room 20rem wide by 10px. `names={false}` leaves them out, and
895
+ the wedges fill the whole circle.
896
+ - The legend writes each value beside its name, because a radius is not read for a quantity.
897
+ `values={false}` leaves the names alone.
898
+ - A wedge the legend hides keeps its place with no radius, so no category moves into another's
899
+ place. The other wedges are measured against the largest value shown.
900
+ - The tooltip writes the wedge's name and value without a heading. The arrows walk the wedges
901
+ clockwise from 12 o'clock.
902
+
903
+ | Prop | Takes | Default |
904
+ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------- |
905
+ | `slices` | `{ key, label, value, color }` per category, in the cycle's order | Required |
906
+ | `label` | The chart's accessible name, recharts' `title` | Required |
907
+ | `caption` | The finding in words, which names the figure | None |
908
+ | `max` | The value of a full radius | The largest value shown |
909
+ | `color` | A hue, a palette or a series color for every wedge | `series.1` |
910
+ | `names` | Whether the names render around the rose | `true` |
911
+ | `values` | Whether the legend writes each value beside its name | `true` |
912
+ | `valueOptions` | `Intl.NumberFormatOptions` for the legend's and the tooltip's values | A number in the locale |
913
+ | `legend` | Whether the legend renders | `true`, while slices exist |
914
+ | `ratio` | The plot's aspect ratio | `square` |
915
+ | `legendLabel`, `defaultIndex`, `animate`, `empty`, the hidden keys, `locale`, `children` | As the cartesian presets take them | |
916
+
917
+ `GaugeChart` renders one value on a dial from a range's minimum to its maximum, with the zones that
918
+ name what the value means:
919
+
920
+ ```tsx
921
+ import { GaugeChart } from "@stealthscale/component-charts";
922
+
923
+ <GaugeChart
924
+ caption="p99 latency is 740ms, inside the 900ms error budget."
925
+ label="Checkout p99 latency"
926
+ max={1200}
927
+ value={740}
928
+ valueOptions={{ style: "unit", unit: "millisecond", unitDisplay: "narrow" }}
929
+ zones={[
930
+ { color: "success", label: "Healthy", upTo: 600 },
931
+ { color: "warning", label: "Watch", upTo: 900 },
932
+ { color: "error", label: "Critical", upTo: 1200 },
933
+ ]}
934
+ />;
935
+ ```
936
+
937
+ - A gauge shows where one value is in a range, such as a latency against its error budget. A bar
938
+ chart compares values with each other.
939
+ - The dial turns clockwise from `startAngle` to `endAngle`, `225` to `-45` in recharts' degrees
940
+ unless stated: three quarters of a turn with the gap at the bottom.
941
+ - The reading fills the dial from `min` in its zone's color, and the rest of the range renders as
942
+ the track in the neutral palette's `subtle` fill. Under forced colors the track fills with
943
+ `Canvas` inside a `GrayText` edge.
944
+ - The chart writes the value in the middle of the dial and the zone's name under it, because an arc
945
+ is not read for a quantity. `centerLabel` replaces the zone's name, such as with a unit.
946
+ - A zone takes `{ upTo, label, color }` and starts where the zone before it ends. The zones form a
947
+ thin ring outside the reading, each at 40% of its color over the panel, a degree apart. A zone
948
+ without a color takes the neutral palette.
949
+ - The part of the range past the last zone renders as the track in the zones' ring. A reading there,
950
+ or in a zone without a color, takes `color`, the theme's first series color unless stated.
951
+ - `gaugeBands(zones, min, max)` returns the bands the ring renders: sorted, clamped to the range,
952
+ and with the range's uncovered part marked `uncovered`. A zone that ends past `max`, `Infinity`
953
+ included, ends at `max`. `gaugeBandAt(bands, value)` returns the band a value is in, the lower one
954
+ on a boundary.
955
+ - A value past the range fills the dial to its end or leaves it empty, and the figure and the
956
+ meter's text write the value itself. A value that is not a finite number renders the empty state.
957
+ - The range's two ends are written under the dial's ends. `limits={false}` leaves them out, for a
958
+ gauge in a small tile.
959
+ - The plot is a `meter` named by `label`. Its text is the value and the zone's name, such as "740ms,
960
+ Watch", and includes a zone's `label` only when the label is a string. The chart has no keyboard
961
+ layer and no tooltip, because the meter reports its one value.
962
+
963
+ | Prop | Takes | Default |
964
+ | ------------------------ | -------------------------------------------------------------------------------- | ---------------------- |
965
+ | `value` | The value the gauge shows | Required |
966
+ | `max` | The value the dial ends at | Required |
967
+ | `label` | The meter's accessible name | Required |
968
+ | `min` | The value the dial starts at | `0` |
969
+ | `zones` | `{ upTo, label, color }` per zone, in any order | None |
970
+ | `caption` | The finding in words, which names the figure | None |
971
+ | `centerLabel` | The words under the figure | The zone's name |
972
+ | `color` | A hue, a palette or a series color for a reading no zone colors | `series.1` |
973
+ | `limits` | Whether the range's ends are written under the dial | `true` |
974
+ | `valueOptions` | `Intl.NumberFormatOptions` for the figure, the range's ends and the meter's text | A number in the locale |
975
+ | `startAngle`, `endAngle` | The angles the dial starts and ends at, in recharts' degrees | `225`, `-45` |
976
+ | `animate` | Whether the dial fills in, which it never does under reduced motion | `false` |
977
+ | `empty` | The message in the plot's place while the value is not a finite number | `No data` |
978
+ | `ratio` | The plot's aspect ratio | `square` |
979
+ | `locale`, `children` | As the cartesian presets take them | |
980
+
981
+ ## Funnel
982
+
983
+ `FunnelChart` renders the stages one set passes through, each a trapezoid as wide as its count,
984
+ above a table of each stage's count and both of its rates:
985
+
986
+ ```tsx
987
+ import { FunnelChart } from "@stealthscale/component-charts";
988
+
989
+ <FunnelChart
990
+ caption="The biggest loss is before “Added to basket”: 18,500 people."
991
+ label="Checkout funnel for September"
992
+ stages={[
993
+ { key: "visited", label: "Visited the shop", value: 48_200 },
994
+ { key: "viewed", label: "Viewed a product", value: 31_400 },
995
+ { key: "basket", label: "Added to basket", value: 12_900 },
996
+ { key: "paid", label: "Paid", value: 5960 },
997
+ ]}
998
+ />;
999
+ ```
1000
+
1001
+ - A funnel shows which step of a process loses people. Its stages are nested sets, each a subset of
1002
+ the one before it. A bar chart compares values that are not nested.
1003
+ - The stages run from the top in their order. Each stage narrows to the next, and the last is a
1004
+ rectangle. A stage's width is its count's share of the first stage's count.
1005
+ - Each stage's count is written on it in the ink inside a halo in the panel's color, and the pointer
1006
+ passes through the count to the stage. `values={false}` leaves the counts out.
1007
+ - The table under the plot writes each stage's count, its share of the stage before and its share of
1008
+ the first stage. The first stage's share of the stage before is empty. `steps={false}` leaves the
1009
+ table out.
1010
+ - The table is the collections package's `Table.Simple` at the small size, and each stage's name
1011
+ follows a swatch of its fill. In a room too narrow for it the table scrolls sideways, and its
1012
+ scroll area is then a region in the tab order named by `stepsLabel`.
1013
+ - A rate is written to two significant digits, such as 0.16%, 12% or 150%, because a whole percent
1014
+ writes a 0.3% conversion as 0%.
1015
+ - The stages take one palette, `color`, from the whole color at the top down to 35% of it over the
1016
+ panel at the bottom, because the stages are ordered.
1017
+ - `funnelSteps(stages)` returns each stage with its count, its two rates and the count lost before
1018
+ it. `biggestDrop(steps)` returns the step that loses the most people, by count rather than by
1019
+ rate, and `funnelWidenings(stages)` returns the stages whose count is above the stage before. A
1020
+ caption states the findings they return.
1021
+ - A stage whose count is above the stage before renders as it is, and its share of the stage before
1022
+ is above 100%. It means the counts come from sets that are not nested, such as two queries
1023
+ filtered apart.
1024
+ - A value that is not a finite number or is below zero counts as 0, and a stage after a stage of 0
1025
+ converts at 0.
1026
+ - The tooltip heads a stage with its name and writes its count and both rates. The arrows walk the
1027
+ stages from the top.
1028
+ - The chart has no legend, because hiding a stage would change both rates of the stage after it.
1029
+ - A pyramid, curved or stepped sections and names beside the stages are not offered. The table
1030
+ writes the names.
1031
+
1032
+ | Prop | Takes | Default |
1033
+ | -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------- |
1034
+ | `stages` | `{ key, label, value }` per stage, from the top | Required |
1035
+ | `label` | The chart's accessible name, recharts' `title` | Required |
1036
+ | `caption` | The finding in words, which names the figure | None |
1037
+ | `color` | A hue, a palette or a series color for the stages | `series.1` |
1038
+ | `values` | Whether each stage's count is written on it | `true` |
1039
+ | `steps` | Whether the table of the stages' counts and rates renders under the plot | `true` |
1040
+ | `valueOptions` | `Intl.NumberFormatOptions` for the counts on the stages, the table and the tooltip | A number in the locale |
1041
+ | `stageLabel`, `valueLabel`, `stepLabel`, `overallLabel` | The headings of the table's columns and the tooltip's rows | `Stage`, `Count`, `Of previous`, `Of first` |
1042
+ | `stepsLabel` | The accessible name of the table's scroll area | `Conversion by stage` |
1043
+ | `ratio` | The plot's aspect ratio | `video` |
1044
+ | `defaultIndex`, `animate`, `empty`, `locale`, `children` | As the cartesian presets take them | |
1045
+
1046
+ ## Treemap
1047
+
1048
+ `TreemapChart` renders a hierarchy of parts as nested tiles whose areas follow their sizes, such as
1049
+ spend by team and then by service:
1050
+
1051
+ ```tsx
1052
+ import { TreemapChart } from "@stealthscale/component-charts";
1053
+
1054
+ <TreemapChart
1055
+ caption="Kubernetes and Postgres are 42% of the bill."
1056
+ label="Cloud spend for September by team and service"
1057
+ nodes={[
1058
+ {
1059
+ children: [
1060
+ { key: "postgres", label: "Postgres", value: 18_400 },
1061
+ { key: "kafka", label: "Kafka", value: 9700 },
1062
+ ],
1063
+ key: "data",
1064
+ label: "Data",
1065
+ },
1066
+ {
1067
+ children: [
1068
+ { key: "kubernetes", label: "Kubernetes", value: 21_800 },
1069
+ { key: "logs", label: "Log ingest", value: 7600 },
1070
+ ],
1071
+ key: "platform",
1072
+ label: "Platform",
1073
+ },
1074
+ ]}
1075
+ valueOptions={{ currency: "EUR", maximumFractionDigits: 0, style: "currency" }}
1076
+ />;
1077
+ ```
1078
+
1079
+ - A treemap shows where the mass of many parts is, in two levels. An area is read less precisely
1080
+ than a length, so a bar chart compares values, and a pie or a donut shows two or three parts.
1081
+ - A parent's size is its children's sum, and its own `value` is ignored. A value that is not a
1082
+ finite number or is below zero counts as 0 and has no tile, because an area cannot be negative.
1083
+ - Each level runs largest first from the top left. The tiles are 2px apart, and the panel shows
1084
+ between them.
1085
+ - Each top-level node is a family of tiles in its `color`, else in the series color at its place.
1086
+ Its parts mix that color over the panel, from the whole color for the largest part down to 45% for
1087
+ the smallest.
1088
+ - Each tile writes its name, and under the name its value and its share of the whole, where the tile
1089
+ is tall enough. The tile measures each line after layout and hides a line wider than itself,
1090
+ because a clipped word reads as another word. `values={false}` leaves the value and the share out.
1091
+ - The legend lists each top-level node with its total. A node the legend hides leaves the layout,
1092
+ and each share is of the nodes shown.
1093
+ - The tooltip heads a tile with its name and writes its value and its share of the total to two
1094
+ significant digits.
1095
+ - The chart's `svg` is one tab stop named by `label`. The left and right arrows walk the tiles depth
1096
+ first, a family before its parts, Home and End go to the first tile and the last, and each step
1097
+ opens the tooltip at the tile. `defaultIndex` is a place in that walk.
1098
+ - `hierarchyLeaves(nodes)` returns every leaf depth first with its top-level group, its value as
1099
+ given and its share of the total of every tile. A ranking sorts the leaves, and a leaf with a
1100
+ value below zero is one the chart leaves out. A caption states the findings they return.
1101
+ - recharts' drill-down, `type="nest"`, is not offered, because its breadcrumbs take no key.
1102
+
1103
+ | Prop | Takes | Default |
1104
+ | ------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------- |
1105
+ | `nodes` | `HierarchyNode`s, `{ key, label, value, color, children }`, top level first | Required |
1106
+ | `label` | The accessible name of the chart's keyboard layer | Required |
1107
+ | `caption` | The finding in words, which names the figure | None |
1108
+ | `values` | Whether each tile writes its value and share, and the legend each family's total | `true` |
1109
+ | `valueOptions` | `Intl.NumberFormatOptions` for the values on the tiles, in the legend and the tooltip | A number in the locale |
1110
+ | `valueLabel`, `shareLabel` | The names of the tooltip's rows | `Value`, `Of total` |
1111
+ | `legend`, `legendLabel` | Whether the legend renders, and the name of its group of buttons | `true`, `Series` |
1112
+ | `defaultIndex` | The place in the walk of the tile the tooltip opens at first | None |
1113
+ | `hiddenKeys`, `defaultHiddenKeys`, `onHiddenKeysChange` | The top-level nodes the legend hides | |
1114
+ | `ratio` | The plot's aspect ratio | `video` |
1115
+ | `animate`, `empty`, `locale`, `children` | As the cartesian presets take them | |
1116
+
1117
+ ## Sunburst
1118
+
1119
+ `SunburstChart` renders a hierarchy of parts as rings around a middle, one ring per level, such as
1120
+ spend by team, then by service, then by resource:
1121
+
1122
+ ```tsx
1123
+ import { SunburstChart } from "@stealthscale/component-charts";
1124
+
1125
+ <SunburstChart
1126
+ caption="The platform team is 65% of the bill."
1127
+ center="€73,500"
1128
+ centerLabel="September"
1129
+ label="Cloud spend for September by team, service and resource"
1130
+ nodes={[
1131
+ {
1132
+ children: [
1133
+ {
1134
+ children: [
1135
+ { key: "vms", label: "Virtual machines", value: 26_000 },
1136
+ { key: "gpus", label: "GPUs", value: 12_400 },
1137
+ ],
1138
+ key: "compute",
1139
+ label: "Compute",
1140
+ },
1141
+ { key: "network", label: "Network", value: 9300 },
1142
+ ],
1143
+ key: "platform",
1144
+ label: "Platform",
1145
+ },
1146
+ {
1147
+ children: [
1148
+ { key: "web", label: "Web app", value: 14_200 },
1149
+ { key: "mobile", label: "Mobile app", value: 11_600 },
1150
+ ],
1151
+ key: "product",
1152
+ label: "Product",
1153
+ },
1154
+ ]}
1155
+ valueOptions={{ currency: "EUR", maximumFractionDigits: 0, style: "currency" }}
1156
+ />;
1157
+ ```
1158
+
1159
+ - A sunburst shows how a whole divides level by level and which parent each part belongs to. An arc
1160
+ is read less precisely than a length or an area, so a bar chart compares values and a treemap
1161
+ compares the sizes of many parts.
1162
+ - The rings run from the middle out, one per level. Each ring renders its nodes in their parents'
1163
+ order, largest first, from 12 o'clock and clockwise, so a part's arc is inside its parent's arc. A
1164
+ leaf that ends above the outer rings leaves a gap of its size in each ring beyond it, and a gap
1165
+ takes no pointer.
1166
+ - The rings split the room from a hole of 36% of the radius to 96% of it evenly. The hole renders
1167
+ `center` and `centerLabel`.
1168
+ - Sizes and families follow the treemap's rules. A parent's size is its children's sum, a value
1169
+ below zero or not finite has no arc, and each top-level node is a family in its `color`, else in
1170
+ the series color at its place.
1171
+ - Each ring is paler than the ring inside it. A part mixes its family's color over the panel by its
1172
+ place among its siblings, from the whole color down to 45%, times 0.82 for each level below the
1173
+ top: 82% to 37% in the second ring, 67% to 30% in the third.
1174
+ - No arc is labelled. The tooltip heads an arc with its name and writes its value and its share of
1175
+ the total to two significant digits. The legend lists each top-level node with its total, and a
1176
+ node it hides leaves the rings.
1177
+ - The chart's `svg` is one tab stop named by `label`. The left and right arrows walk the arcs depth
1178
+ first, a family before its parts, Home and End go to the first arc and the last, and each step
1179
+ opens the tooltip at the arc. `defaultIndex` is a place in that walk.
1180
+ - The chart renders a recharts `Pie` per ring inside a `PieChart`, so `children` are recharts
1181
+ elements of a pie chart.
1182
+
1183
+ | Prop | Takes | Default |
1184
+ | ------------------------------------------------------- | --------------------------------------------------------------------------- | ---------------------- |
1185
+ | `nodes` | `HierarchyNode`s, `{ key, label, value, color, children }`, top level first | Required |
1186
+ | `label` | The accessible name of the chart's keyboard layer | Required |
1187
+ | `caption` | The finding in words, which names the figure | None |
1188
+ | `center`, `centerLabel` | The figure in the hole and its words | None |
1189
+ | `values` | Whether the legend writes each family's total | `true` |
1190
+ | `valueOptions` | `Intl.NumberFormatOptions` for the values in the legend and the tooltip | A number in the locale |
1191
+ | `valueLabel`, `shareLabel` | The names of the tooltip's rows | `Value`, `Of total` |
1192
+ | `legend`, `legendLabel` | Whether the legend renders, and the name of its group of buttons | `true`, `Series` |
1193
+ | `defaultIndex` | The place in the walk of the arc the tooltip opens at first | None |
1194
+ | `hiddenKeys`, `defaultHiddenKeys`, `onHiddenKeysChange` | The top-level nodes the legend hides | |
1195
+ | `ratio` | The plot's aspect ratio | `square` |
1196
+ | `animate`, `empty`, `locale`, `children` | As the cartesian presets take them | |
1197
+
1198
+ ## Sankey
1199
+
1200
+ `SankeyChart` renders flows between the stages of a process as bands whose widths follow their
1201
+ values, such as a month's visitors by channel, then by what they did:
1202
+
1203
+ ```tsx
1204
+ import { SankeyChart } from "@stealthscale/component-charts";
1205
+
1206
+ <SankeyChart
1207
+ caption="27% of visitors signed up."
1208
+ flows={[
1209
+ { from: "organic", to: "signup", value: 3100 },
1210
+ { from: "organic", to: "left", value: 9300 },
1211
+ { from: "paid", to: "signup", value: 2000 },
1212
+ { from: "paid", to: "left", value: 4800 },
1213
+ ]}
1214
+ label="Visitors for September by channel and outcome"
1215
+ nodes={[
1216
+ { key: "organic", label: "Organic search" },
1217
+ { key: "paid", label: "Paid search" },
1218
+ { key: "signup", label: "Signed up" },
1219
+ { key: "left", label: "Left" },
1220
+ ]}
1221
+ />;
1222
+ ```
1223
+
1224
+ - A sankey shows where a quantity goes as it splits and merges between stages. A funnel is the
1225
+ better chart for one path.
1226
+ - `nodes` are `{ key, label, color }` and `flows` are `{ from, to, value }` by key, where recharts
1227
+ takes indexes. A flow is rendered between two known nodes with a finite value above zero, and
1228
+ flows between the same two nodes add up to one band. The chart leaves out a node without a
1229
+ rendered flow.
1230
+ - `sankeyCycles(flows)` returns the flows that close a loop against the flows before them. A sankey
1231
+ shows flow in one direction and recharts throws on a loop, so the chart leaves those flows out,
1232
+ and a caption states them.
1233
+ - `flowBalance(nodes, flows)` returns each node's inflow and outflow. A node that sends less than it
1234
+ receives loses flow the chart does not show, and a caption states the difference.
1235
+ - Each node is a bar in its `color`, else in the series color at its place, as tall as the larger of
1236
+ its inflow and outflow. Each flow is a band in its source's color at the backdrop opacity.
1237
+ recharts places each node in a column by the longest path to it, and a node that sends nothing in
1238
+ the last column.
1239
+ - Each node writes its name after its bar, or before it in the last column, and its value under the
1240
+ name where the bar is two lines tall (`values`). A line that would meet a line of a node earlier
1241
+ in `nodes`, or leave the plot, is hidden. The tooltip and the walk name every node.
1242
+ - The tooltip writes a node's inflow and outflow, and a flow's value and its share of what its
1243
+ source sends to two significant digits. The tooltip at a flow lifts it, and at a node lifts every
1244
+ flow of the node, while the other marks fade.
1245
+ - The chart's `svg` is one tab stop named by `label`. The left and right arrows walk each node in
1246
+ the order of `nodes`, then the flows it sends. Home and End go to the first mark and the last, and
1247
+ each step opens the tooltip at the mark. `defaultIndex` is a place in that walk.
1248
+
1249
+ | Prop | Takes | Default |
1250
+ | ----------------------------- | ----------------------------------------------------------------------------- | ---------------------- |
1251
+ | `nodes` | `SankeyNode`s, `{ key, label, color }`, in the walk's order | Required |
1252
+ | `flows` | `SankeyFlow`s, `{ from, to, value }` by key | Required |
1253
+ | `label` | The accessible name of the chart's keyboard layer | Required |
1254
+ | `caption` | The finding in words, which names the figure | None |
1255
+ | `values` | Whether each node writes its value under its name | `true` |
1256
+ | `valueOptions` | `Intl.NumberFormatOptions` for the values beside the nodes and in the tooltip | A number in the locale |
1257
+ | `inflowLabel`, `outflowLabel` | The names of a node's rows in the tooltip | `In`, `Out` |
1258
+ | `valueLabel`, `shareLabel` | The names of a flow's rows in the tooltip | `Value`, `Of source` |
1259
+ | `defaultIndex` | The place in the walk of the mark the tooltip opens at first | None |
1260
+ | `ratio` | The plot's aspect ratio | `video` |
1261
+ | `empty`, `locale`, `children` | As the cartesian presets take them | |
1262
+
1263
+ ## Chord diagram
1264
+
1265
+ `ChordDiagram` renders flows between peers around a circle, such as calls between services. Each
1266
+ node is an arc as long as what it sends, and each pair of nodes is one ribbon whose two ends are as
1267
+ wide as the two directions:
1268
+
1269
+ ```tsx
1270
+ import { ChordDiagram } from "@stealthscale/component-charts";
1271
+
1272
+ <ChordDiagram
1273
+ caption="api sends the most calls."
1274
+ flows={[
1275
+ { from: "api", to: "auth", value: 2600 },
1276
+ { from: "auth", to: "api", value: 1900 },
1277
+ { from: "api", to: "billing", value: 1400 },
1278
+ { from: "billing", to: "api", value: 700 },
1279
+ ]}
1280
+ label="Calls between services in the last hour"
1281
+ nodes={[
1282
+ { key: "api", label: "api" },
1283
+ { key: "auth", label: "auth" },
1284
+ { key: "billing", label: "billing" },
1285
+ ]}
1286
+ />;
1287
+ ```
1288
+
1289
+ - A chord diagram shows the flows a sankey leaves out: flows both ways between two nodes, flows
1290
+ around a loop, and a node's flow to itself. A sankey is the better chart for flow through stages.
1291
+ - `nodes` and `flows` are the sankey's `SankeyNode` and `SankeyFlow`, so the same arrays render as
1292
+ either chart. A flow counts between two known nodes with a finite value above zero, and flows
1293
+ between the same two nodes add up. Only the nodes a counted flow touches have arcs.
1294
+ - `chordLayout(nodes, flows, pad)` returns the arcs and the ribbons in radians from 12 o'clock,
1295
+ clockwise, as d3-chord's. An arc is as long as what its node sends, so a node that only receives
1296
+ has an arc of no length and its ribbons end in points. The arcs are 0.06 radians apart unless
1297
+ `pad` states another space, and the spaces take at most half the circle.
1298
+ - Each arc is in its node's `color`, else in the series color at its place. Each ribbon is wider at
1299
+ the node that sends more and takes that node's color, so its color is the pair's net direction. A
1300
+ ribbon rests at the backdrop opacity.
1301
+ - Each node writes its name outside the ring at its arc's middle. A name that would meet the name of
1302
+ a node earlier in `nodes`, or leave the plot's sides, is hidden. The readout and the walk name
1303
+ every node.
1304
+ - The readout in the middle of the ring is the kit's tooltip. It writes what an arc's node sends and
1305
+ receives, and both directions of a ribbon, including a direction of 0. The readout at a ribbon
1306
+ lifts it, and at an arc every ribbon of its node, while the other marks fade.
1307
+ - The chart's `svg` is one tab stop named by `label`. The left and right arrows walk each arc in the
1308
+ order of `nodes`, then the ribbons that start on it. Home and End go to the first mark and the
1309
+ last, and each step moves the readout to the mark. `defaultIndex` is a place in that walk.
1310
+
1311
+ | Prop | Takes | Default |
1312
+ | ----------------------------- | --------------------------------------------------------------------------- | ---------------------- |
1313
+ | `nodes` | `SankeyNode`s, `{ key, label, color }`, in the order their arcs follow | Required |
1314
+ | `flows` | `SankeyFlow`s, `{ from, to, value }` by key, both ways and to a node itself | Required |
1315
+ | `label` | The accessible name of the chart's keyboard layer | Required |
1316
+ | `caption` | The finding in words, which names the figure | None |
1317
+ | `valueOptions` | `Intl.NumberFormatOptions` for the amounts in the readout | A number in the locale |
1318
+ | `inflowLabel`, `outflowLabel` | The names of an arc's rows in the readout | `In`, `Out` |
1319
+ | `defaultIndex` | The place in the walk of the mark the readout opens at first | None |
1320
+ | `ratio` | The plot's aspect ratio | `square` |
1321
+ | `empty`, `locale`, `children` | As the cartesian presets take them | |
1322
+
1323
+ ## Heatmap
1324
+
1325
+ `Heatmap` renders a value for every pair of a row and a column as a grid of cells colored from their
1326
+ values, such as card authorisations per weekday and hour:
1327
+
1328
+ ```tsx
1329
+ import { Heatmap } from "@stealthscale/component-charts";
1330
+
1331
+ <Heatmap
1332
+ caption="Tuesday at 17:00 is the busiest hour, with 405 authorisations."
1333
+ cells={[
1334
+ { column: "09", row: "mon", value: 128 },
1335
+ { column: "17", row: "mon", value: 342 },
1336
+ { column: "09", row: "tue", value: 116 },
1337
+ { column: "17", row: "tue", value: 405 },
1338
+ ]}
1339
+ columns={[
1340
+ { key: "09", label: "09:00" },
1341
+ { key: "17", label: "17:00" },
1342
+ ]}
1343
+ corner="Weekday"
1344
+ label="Card authorisations per hour, by weekday"
1345
+ rows={[
1346
+ { key: "mon", label: "Mon" },
1347
+ { key: "tue", label: "Tue" },
1348
+ ]}
1349
+ valueLabel="Authorisations"
1350
+ />;
1351
+ ```
1352
+
1353
+ - A reader finds a dark block in a heatmap faster than the largest number in a table, and reads the
1354
+ exact value in the readout or printed in the cell.
1355
+ - The grid is an HTML `table` in the `grid` role, named by `label`. Each heading is a `th` with its
1356
+ scope, so a screen reader reads a cell's value with its row and its column.
1357
+ - `cells` are `{ row, column, value }`. Every pair of a row and a column is a cell. A pair without a
1358
+ reading, a `null` value and a value that is not a finite number are missing: the cell has no fill,
1359
+ a dashed edge and the words of `missingLabel`. Of two readings for one pair, the later applies.
1360
+ - `rows` and `columns` list the headings in order. An axis without them takes each key in the order
1361
+ the cells first name it. A reading whose row or column is not on a stated axis is left out.
1362
+ - `sparse` renders a pair without a reading as an empty place: no fill, no edge, no words, and the
1363
+ walk passes over it. A `null` value is still a missing reading. A calendar's days outside its
1364
+ window and the periods past a cohort's last count are empty places.
1365
+ - `groups` lists the headings of groups of columns, and a column states its group's key in `group`.
1366
+ A group's heading spans the consecutive columns that name it in a row above the column headings.
1367
+ Its words start at the group's first column, never widen the columns, and are hidden from sight
1368
+ while they are wider than the group.
1369
+ - A heading with `hidden` renders its words for a screen reader alone, such as a calendar's weeks.
1370
+ - A reading's `label` names it: the readout writes it as its heading in place of the row's and the
1371
+ column's words, and a screen reader reads it before the value, after the locale's list separator.
1372
+ A reading's `text` is what the cell prints and reads for its value, such as a head count while the
1373
+ fill is a rate.
1374
+ - `shape` renders each cell as a `block`, which widens to a printed value, or a `square` with a 2px
1375
+ corner, which does not print its value. Beside squares the row headings take no height of their
1376
+ own, so the rows are as far apart as the columns. A square keeps its side while its column's
1377
+ heading fits it.
1378
+ - The cell type is the caller's: `onSelect` receives the reading as it was passed, with any fields
1379
+ the caller added, such as a calendar day's `date`. A pair without a reading calls nothing.
1380
+ - A `sequential` scale mixes `color` over the panel from 12% at the domain's minimum to the whole
1381
+ color at its maximum, so the lowest value keeps a tint. A `diverging` scale mixes
1382
+ `colors.negative` under `midpoint` and `colors.positive` over it, from the panel at the midpoint
1383
+ to the whole color at the further end of the domain, with one reach on both sides. A value past
1384
+ the domain takes the color of its end.
1385
+ - The domain is the values' span unless `domain` pins it. `heatmapDomain(cells)` returns the span of
1386
+ the readings of two or more grids, so grids read against each other take one domain.
1387
+ - `values` prints each value in its cell in black or white, whichever `contrast-color()` finds
1388
+ contrasts more with the cell: at least 4.56:1 on every fill of the ten themes. A browser without
1389
+ `contrast-color()` writes the values in the ink.
1390
+ - The key under the grid writes `valueLabel` and the domain's ends on either side of a bar of the
1391
+ scale's colors, and a diverging scale's midpoint under the bar. The bar is hidden from assistive
1392
+ technology.
1393
+ - The grid is one tab stop. The arrow keys move to the next cell, Home and End to the row's ends,
1394
+ and Control or Command with Home or End to the grid's first and last cell. Enter, Space and a
1395
+ press call `onSelect` with the cell's reading.
1396
+ - The readout over the cell under the pointer, else over the focused cell, writes the reading's
1397
+ `label`, else the row's and the column's headings, and the value. Escape hides it until the
1398
+ pointer or focus moves.
1399
+ - A grid wider than its container scrolls sideways, with the row headings sticky at its start. The
1400
+ cell at `defaultIndex` is scrolled into the grid's view when the heatmap mounts, and the page does
1401
+ not move.
1402
+
1403
+ | Prop | Takes | Default |
1404
+ | ----------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------- |
1405
+ | `cells` | `HeatmapCell`s, `{ row, column, value, label, text }`, in any order | Required |
1406
+ | `label` | The grid's accessible name | Required |
1407
+ | `rows`, `columns` | `HeatmapHeading`s, `{ key, label, group, hidden }`, in order | Each key in the order the cells first name it |
1408
+ | `groups` | `HeatmapHeading`s of the groups of columns | None |
1409
+ | `sparse` | Whether a pair without a reading is an empty place | `false` |
1410
+ | `corner` | The words over the row headings | None |
1411
+ | `caption` | The finding in words, which names the figure | None |
1412
+ | `scale` | `sequential` or `diverging` | `sequential` |
1413
+ | `color` | The color of a sequential scale | `series.1` |
1414
+ | `colors` | `{ negative, positive }`, the colors of a diverging scale | `orange` and `blue` |
1415
+ | `midpoint` | The value a diverging scale takes the panel at | `0` |
1416
+ | `domain` | `{ min, max }`, the values the scale spans | The values' span |
1417
+ | `values` | Whether each block cell prints its value | `false` |
1418
+ | `valueOptions` | `Intl.NumberFormatOptions` for the values | A number in the locale |
1419
+ | `valueLabel` | The name of the values in the key and the readout | `Value` |
1420
+ | `missingLabel` | The words of a cell without a value | `No data` |
1421
+ | `defaultIndex` | The place in `cells` of the reading the readout shows at first | None |
1422
+ | `onSelect` | Called with the reading of the cell Enter, Space or a press selects | None |
1423
+ | `shape` | `block` or `square` | `block` |
1424
+ | `size` | `sm`, `md` or `lg`: blocks 24, 32 or 44px tall and at least as wide, squares 10, 14 or 24px | `md` |
1425
+ | `ratio` | The aspect ratio of the empty state | `wide` |
1426
+ | `empty`, `locale` | As the cartesian presets take them | |
1427
+
1428
+ ### Calendar
1429
+
1430
+ `calendarCells(days, options)` lays daily readings out as a heatmap calendar and returns the props
1431
+ to spread into `Heatmap`:
1432
+
1433
+ ```tsx
1434
+ import { calendarCells, Heatmap } from "@stealthscale/component-charts";
1435
+
1436
+ <Heatmap
1437
+ {...calendarCells(deploys, { from: "2025-10-01", locale: "en-US", to: "2026-09-30" })}
1438
+ caption="The release week of 9 March was the busiest, with 80 deploys."
1439
+ label="Deploys per day, October 2025 to September 2026"
1440
+ size="sm"
1441
+ valueLabel="Deploys"
1442
+ />;
1443
+ ```
1444
+
1445
+ - `days` are `{ date, value }`, each date a civil date written `YYYY-MM-DD`, never an instant. The
1446
+ layout reads each date as a calendar date through `@internationalized/date`, so no time zone moves
1447
+ a day. A string that is not a date is left out, and of two readings for one date the later
1448
+ applies.
1449
+ - The window runs from `from` to `to`, the days' span unless stated, in whole weeks. A day of the
1450
+ window without a reading is missing, and a place of the first or last week outside the window is
1451
+ an empty place.
1452
+ - The rows are the weekdays from the locale's first day of the week, or from `weekStartsOn`, a
1453
+ `DayOfWeek` from `sun` to `sat`. The columns are the weeks, each with a hidden heading of its
1454
+ dates in the window. The groups are the months, each week in the month of its first day in the
1455
+ window, the first month with its year.
1456
+ - Each cell's `label` is its date in the locale's full words, and `onSelect` receives the cell with
1457
+ its `date`.
1458
+ - The props render square cells in a sparse grid. A calendar that calls `onSelect` takes
1459
+ `size="lg"`, whose 24px squares are the pointer target WCAG 2.5.8 asks for.
1460
+
1461
+ ## Cohorts
1462
+
1463
+ A cohort is `{ key, label, size, retained }`: how many arrived together and how many remained at
1464
+ each period since, index 0 being the period they arrived in. The length of `retained` is how far the
1465
+ cohort is observed. A `null` inside it is a count nobody measured, and a period past its end has not
1466
+ happened yet.
1467
+
1468
+ `cohortCells(cohorts, options)` returns the props of a heatmap of retention by cohort:
1469
+
1470
+ ```tsx
1471
+ import { cohortCells, Heatmap } from "@stealthscale/component-charts";
1472
+
1473
+ <Heatmap
1474
+ {...cohortCells(signups, {
1475
+ average: "All cohorts",
1476
+ label: (cohort) => `${cohort.label} · ${cohort.size}`,
1477
+ locale: "en-US",
1478
+ periodLabel: (period) => `M${period}`,
1479
+ })}
1480
+ corner="Signed up"
1481
+ label="Share of customers still active, by signup month and month since signup"
1482
+ values
1483
+ />;
1484
+ ```
1485
+
1486
+ - Each cohort is a row and each period a column. A cell's value is the share of the cohort that
1487
+ remained, on a scale from 0 to 1 written as a percentage, and a period past the cohort's last
1488
+ count is an empty place.
1489
+ - `measure: "count"` writes how many remained in each cell, and the fill is the rate either way.
1490
+ - `average` names a last row of the rates weighted by each cohort's size, over the cohorts with a
1491
+ rate at the period. `cohortAverages` returns those rates, and `retentionRate` one cohort's rate.
1492
+ - `onSelect` receives a cell with its `cohort`, `period` and `retained` count.
1493
+
1494
+ `retentionSeries(cohorts, options)` returns the props of a `LineChart` of the same cohorts:
1495
+
1496
+ ```tsx
1497
+ import { LineChart, retentionSeries } from "@stealthscale/component-charts";
1498
+
1499
+ <LineChart
1500
+ {...retentionSeries(trials, { periodLabel: (week) => `W${week}` })}
1501
+ label="Share of trials still active, by week since the trial started"
1502
+ />;
1503
+ ```
1504
+
1505
+ - A row per period, a line per cohort, and the average weighted by each cohort's size. The value
1506
+ axis runs from 0 to 1 and writes percentages.
1507
+ - Every cohort's line takes `color`, `series.1` unless stated, each newer cohort mixed further
1508
+ towards the ink, up to 60% for the newest. The average is the ink.
1509
+ - The average renders solid while at least `minCohorts`, 3 unless stated, have a rate at the period,
1510
+ and dashed after, where it is the average of the oldest few. The solid and the dashed part share
1511
+ the last solid period, so the line joins. `averageLabel` and `sparseLabel` name them.
1512
+ - `cohortSeriesKey(key)` returns a cohort's series key, for `defaultHiddenKeys` or a
1513
+ `ReferenceLine`.
1514
+
1515
+ ## Timeline
1516
+
1517
+ `TimelineChart` places moments on one time window across lanes, such as deploys and incidents per
1518
+ service:
1519
+
1520
+ ```tsx
1521
+ import { TimelineChart } from "@stealthscale/component-charts";
1522
+
1523
+ <TimelineChart
1524
+ caption="The api raised three alerts between 14:03 and 14:09 UTC and rolled back at 16:20."
1525
+ events={moments}
1526
+ label="Deploys and incidents on 28 September"
1527
+ labelOptions={{ hour: "2-digit", hourCycle: "h23", minute: "2-digit", timeZone: "UTC" }}
1528
+ lanes={[
1529
+ { key: "api", label: "API" },
1530
+ { key: "web", label: "Web" },
1531
+ ]}
1532
+ since="2026-09-28T00:00:00Z"
1533
+ until="2026-09-29T00:00:00Z"
1534
+ />;
1535
+ ```
1536
+
1537
+ - A moment is `{ key, at, label, lane, color }`, where `at` is a `Date`, milliseconds since the
1538
+ epoch or an ISO 8601 string. A moment whose instant cannot be read, or outside the window, is left
1539
+ out.
1540
+ - `lanes` lists the lanes in order with their names, else each lane in the order the moments first
1541
+ name it, and a moment of a lane it does not list is left out. The moments without a lane go into
1542
+ one lane named by `otherLabel`, after the others unless `lanes` lists its key, `OTHER_LANE`. A
1543
+ chart of one lane renders no lane axis.
1544
+ - Every lane shares one window, from the earliest moment to the latest unless `since` and `until`
1545
+ state it. A caller who shows part of the moments states the window, so each marker keeps its
1546
+ place.
1547
+ - Moments of a lane closer than `minGap` of the window share one marker. A moment joins the marker
1548
+ that opened first, measured from where that marker opened, so evenly spaced moments do not chain
1549
+ into one marker across the chart.
1550
+ - A marker is a dot of radius 6 for one moment and a pill 20px tall with the count for two or more,
1551
+ 7px wider per digit of the count. It takes the color of its loudest moment: `error`, then
1552
+ `warning`, then its first moment's color, else the theme's first series color. The count is black
1553
+ or white, whichever `contrast-color()` finds against the marker.
1554
+ - The time axis writes `ticks` times evenly spaced across the window, both ends included, in
1555
+ `labelOptions`. recharts moves the time at each end inward until it fits, and leaves out a time
1556
+ between them that would meet another.
1557
+ - The tooltip heads a marker with its lane and its time, or the range from its first moment to its
1558
+ last, and lists each moment with its time: five at most, then the row `moreLabel` writes for the
1559
+ rest.
1560
+ - The plot is a row of `sizes.10` per lane over the time axis, at the chart's `rows` ratio.
1561
+ - The chart's `svg` is one tab stop in the `application` role. The left and right arrows, Home and
1562
+ End walk the markers lane by lane, and Enter and Space select the marker the walk is at.
1563
+ `onSelect` receives the marker's moments on a press, Enter or Space.
1564
+ - `layoutEvents(events, options)` returns the window and the lanes of clusters the chart plots, for
1565
+ a caller who writes a caption from them.
1566
+ - To show part of the moments, a caller filters `events` and states `since` and `until`, so the
1567
+ window does not move.
1568
+
1569
+ | Prop | Takes | Default |
1570
+ | ---------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------- |
1571
+ | `events` | The moments, in any order | Required |
1572
+ | `label` | The chart's accessible name, recharts' `title` | Required |
1573
+ | `caption` | The finding in words, which names the figure | None |
1574
+ | `lanes` | `{ key, label }` per lane, in order | Each lane the moments name |
1575
+ | `otherLabel` | The name of the lane of the moments without a lane | `Other` |
1576
+ | `since`, `until` | The window's ends: a `Date`, milliseconds or an ISO 8601 string | The earliest and the latest moment |
1577
+ | `minGap` | The share of the window under which two moments of a lane share a marker | `0.02` |
1578
+ | `ticks` | The number of times on the time axis, both ends included | `5` |
1579
+ | `labelOptions` | `Intl.DateTimeFormatOptions` for the ticks and the tooltip's times | The day, the month and the time |
1580
+ | `moreLabel` | Writes the row that counts the moments the tooltip does not list | `N more` |
1581
+ | `onSelect` | Called with a marker's moments on a press, Enter or Space | None |
1582
+ | `defaultIndex` | The place in the walk of the marker the tooltip shows on the first render | None |
1583
+ | `animate`, `empty`, `locale`, `children` | As the cartesian presets take them | |
1584
+
1585
+ ## Sparkline and sparkbar
1586
+
1587
+ `Sparkline` and `Sparkbar` render a run of values the size of a word, without axes, a tooltip or a
1588
+ legend, for a table row or for the figure the run belongs to:
1589
+
1590
+ ```tsx
1591
+ import { Sparkline } from "@stealthscale/component-charts";
1592
+
1593
+ <Sparkline values={[5980, 6420, 6110, 7030, 6890, 7560, 8130]} />;
1594
+ ```
1595
+
1596
+ - A sparkline spans the run's own range, so it shows whether the run rises or falls, and the figure
1597
+ goes beside it. `area` fills under the line with the chart's gradient, which measures volume only
1598
+ for a run that starts at zero. `curve` is `monotone` unless stated, or `linear`.
1599
+ - A sparkbar measures each bar from zero. `signed` renders a value under zero in the error palette's
1600
+ chart color, for a change per period.
1601
+ - `baseline` marks a value with a dashed line in the subtle ink, and a sparkline's range widens to
1602
+ include it.
1603
+ - A value that is not a finite number keeps its place as a gap, because a zero there would plot a
1604
+ fall that never happened. A sparkbar renders a zero and a missing value alike, as an empty period.
1605
+ - `color` takes a hue or a palette, else the theme's first series color.
1606
+ - `label` makes the run an image named by it. Without one the run is hidden from assistive
1607
+ technology, for a run beside its figure. Neither takes a tab stop.
1608
+ - The box is a `div`, because recharts renders `div`s inside it. A spark goes in a table cell or a
1609
+ row of a layout, not in a paragraph.
1610
+
1611
+ | Axis | Values | Default |
1612
+ | --------- | ---------------------------------------------------------------------- | ------- |
1613
+ | `size` | `sm` 64 by 16, `md` 96 by 24, `lg` 128 by 32 pixels, times the density | `md` |
1614
+ | `stretch` | `true` fills the container's width at the size's height | `false` |
1615
+
1616
+ ## Chart
1617
+
1618
+ `Chart.useChart` takes the rows and the series and returns the chart. `Chart.Root` provides it to
1619
+ the parts. Each mark reads its color, its opacity and whether the legend hides it from the chart.
1620
+
1621
+ ```tsx
1622
+ import { Line, LineChart, Tooltip, XAxis, YAxis } from "recharts";
1623
+
1624
+ import { Chart } from "@stealthscale/component-charts";
1625
+
1626
+ const chart = Chart.useChart({
1627
+ data: payouts,
1628
+ series: [
1629
+ { key: "paid", label: "Paid" },
1630
+ { key: "refunded", label: "Refunded" },
1631
+ ],
1632
+ });
1633
+ const euros = chart.formatNumber({ currency: "EUR", style: "currency" });
1634
+
1635
+ <Chart.Root chart={chart}>
1636
+ <Chart.Plot>
1637
+ <LineChart accessibilityLayer data={chart.data} title="Payouts per day">
1638
+ <XAxis dataKey="day" />
1639
+ <YAxis tickFormatter={euros} />
1640
+ <Tooltip content={<Chart.Tooltip formatValue={euros} />} />
1641
+ {chart.series.map((series) => (
1642
+ <Line
1643
+ dataKey={series.key}
1644
+ hide={series.hidden}
1645
+ key={series.key}
1646
+ opacity={series.opacity}
1647
+ stroke={series.color}
1648
+ />
1649
+ ))}
1650
+ </LineChart>
1651
+ </Chart.Plot>
1652
+ <Chart.Legend />
1653
+ <Chart.Caption>Refunds rose to €1,250 on Thursday.</Chart.Caption>
1654
+ </Chart.Root>;
1655
+ ```
1656
+
1657
+ | Part | Element | What it renders |
1658
+ | ----------- | ------------ | ------------------------------------------------------------------------------------------------------------- |
1659
+ | `Root` | `figure` | The figure. It takes the chart and the ratio |
1660
+ | `Plot` | `div` | The box recharts sizes the chart to, while the chart has rows, and `center` and `centerLabel` over its middle |
1661
+ | `Empty` | `div` | Its children in the plot's place and at its ratio, while the chart has no rows |
1662
+ | `Legend` | `fieldset` | A button per series, which shows and hides the series |
1663
+ | `Key` | `ul` | The parts of the marks, for a chart read by convention, such as a box plot |
1664
+ | `KeyItem` | `li` | A part's glyph, the caller's `svg`, beside its name |
1665
+ | `Caption` | `figcaption` | The finding in words, which is the figure's accessible name |
1666
+ | `Tooltip` | `output` | The content of recharts' `Tooltip`: the category and each series' value, in the legend's order |
1667
+ | `Crosshair` | `g` | A dashed guide across the plot at the active row's value of `dataKey`, a child of a cartesian chart |
1668
+
1669
+ `Chart.Tooltip` takes `rowsOf(entries)` for a tooltip about one datum with more than one number,
1670
+ such as a box. The rows it returns render as `{ key, name, value }` without a swatch.
1671
+ `notesOf(label)` returns notes the tooltip lists after the rows, each `{ key, text, color }` with a
1672
+ swatch.
1673
+
1674
+ `Chart.Crosshair` is a recharts child of any cartesian chart, a preset included:
1675
+ `<LineChart …><Chart.Crosshair dataKey="p95" /></LineChart>`. It renders a guide in 3px dashes in
1676
+ the `border` ink, across the plot at the active row's value, while the tooltip shows a finite value
1677
+ of a series the legend shows. recharts' cursor marks the category, so the two lines cross at the
1678
+ point, and the arrows move both with the tooltip.
1679
+
1680
+ | Axis | Values | Default |
1681
+ | ------- | --------------------------------------------------------------------------------- | ------- |
1682
+ | `ratio` | `square`, `landscape`, `portrait`, `golden`, `video`, `wide`, `ultrawide`, `rows` | `video` |
1683
+
1684
+ recharts renders nothing in a box 0 pixels high, and a plot inside a parent without a height
1685
+ measures 0. The ratio gives the plot its height before recharts measures it. `rows` sizes the plot
1686
+ by its rows in place of its width: `sizes.10` per row of the plot's `--chart-rows`, and `sizes.12`
1687
+ for the axis under them.
1688
+
1689
+ The recipe sets recharts' own inks from the theme: the tick labels and a `Label`'s text in
1690
+ `fg.muted`, the axis lines and tick marks in `border`, the grid in `border.muted`, and the band
1691
+ under the pointer in `neutral.subtle`. These apply over a `fill` or `stroke` passed to an axis or a
1692
+ `Label`.
1693
+
1694
+ ### Series and colors
1695
+
1696
+ A series takes a `key`, the field of each row it reads, a `label` for the tooltip and the legend,
1697
+ and an optional `color`.
1698
+
1699
+ - A series without a color takes the theme's series color at its position, `series.1` to `series.8`.
1700
+ The ninth series takes the first again.
1701
+ - Each theme takes its series colors from its own colors first, so a chart's first series wear the
1702
+ brand. The first series is blue in ink, red in cinder and green in pine, and neon's first three
1703
+ are violet, pink and yellow.
1704
+ - `color` names one of the theme's hue palettes, or one of its eight semantic palettes for a series
1705
+ with a meaning, such as an `error` threshold. The chart renders the palette's `chart` role, the
1706
+ lightest color of the palette's hue that keeps 3:1 against the page and the panel.
1707
+ - `color` may also be one of the series colors, `series.1` to `series.8`, which a series keeps at
1708
+ any position, such as the same series in two charts.
1709
+ - Each theme's contrast check measures every `chart` role and every series color at 3:1 or more
1710
+ against the page and the panel.
1711
+ - `Chart.colorOf(color)` returns a palette's chart color for a recharts child, and
1712
+ `chart.color(key)` a series' color.
1713
+ - `ink` mixes a series' color towards the ink, `fg`, by a share in percent, so series of one color
1714
+ read in an order of strength. A chart color mixed towards the ink measured 3:1 or more against the
1715
+ page and the panel at every share, on the 27 chart colors of the ten themes in both modes.
1716
+
1717
+ A pie's sectors are series: each row's name is a series key, and the caller sets each sector's
1718
+ `fill` from `chart.color`. A `Pie` takes `rootTabIndex={-1}`, because recharts makes the pie's group
1719
+ a tab stop without a name beside the keyboard layer.
1720
+
1721
+ ### The legend
1722
+
1723
+ The legend renders a button per series outside the plot, so switching a series does not resize the
1724
+ chart.
1725
+
1726
+ - A plain press switches the pressed series and never hides the last one shown.
1727
+ - A press with Ctrl or Cmd shows the pressed series alone, and a second one shows every series
1728
+ again.
1729
+ - The pointer or focus on a button fades every other series' marks to the plot's `--chart-faded`,
1730
+ the theme's `opacity.backdrop`, over the fast duration. A mark fades through its `opacity` prop,
1731
+ from `series.opacity` or `chart.opacity(key)`.
1732
+ - A hidden series' name is struck through and its swatch faded. Each swatch is the data package's
1733
+ `ColorSwatch`.
1734
+ - `hiddenKeys` and `onHiddenKeysChange` control the hidden series, and `defaultHiddenKeys` sets them
1735
+ for the first render.
1736
+
1737
+ ### Formatting
1738
+
1739
+ `chart.formatNumber(options)` and `chart.formatDate(options)` take `Intl`'s own options and return
1740
+ functions for recharts' `tickFormatter` and the tooltip's `formatValue` and `formatLabel`. Both
1741
+ format in the chart's `locale` when it is set. Without it they use the locale of the nearest
1742
+ `LocaleProvider`, and without a provider the runtime's. `chart.locale` returns that locale for
1743
+ another `Intl` format. The tooltip formats values as numbers in that locale unless `formatValue` is
1744
+ given, and a band's two ends as a range.
1745
+
1746
+ ### Accessibility
1747
+
1748
+ - The caption is the figure's accessible name. A screen reader reads nothing from a plot's paths, so
1749
+ the caption states the finding: "Refunds doubled after Tuesday's release", not "Line chart of
1750
+ refunds". The root points `aria-labelledby` at the caption while one renders, because Chromium
1751
+ computes a `figure`'s name from its `figcaption` only through `aria-labelledby`.
1752
+ - recharts' keyboard layer, `accessibilityLayer`, is on by default. It makes the chart one tab stop
1753
+ in the `application` role, whose left and right arrows move the tooltip through the categories.
1754
+ The `title` prop gives the chart its accessible name. VoiceOver users turn QuickNav off to use the
1755
+ arrows.
1756
+ - The treemap, the sunburst, the sankey, the chord diagram and the timeline walk their marks
1757
+ themselves. recharts gives a treemap no keyboard layer, the arrows of a pie chart's layer move
1758
+ through its first pie alone, a sankey's layer ignores every key, and the marks of a chord diagram
1759
+ and a timeline are not recharts' own. Each chart's `svg` is the one tab stop in the `application`
1760
+ role. The left and right arrows, Home and End send the pointer's events to the next mark, and the
1761
+ tooltip opens there as it does under a pointer. Enter and Space press the mark the walk is at,
1762
+ which selects a timeline's marker.
1763
+ - A chord diagram's readout is the same tooltip in the middle of the ring. Its panel is transparent
1764
+ while it is empty and remains in the accessibility tree. A screen reader announces the first step
1765
+ of the walk from it.
1766
+ - A heatmap is an HTML table in the `grid` role, where the other charts are an `svg` in the
1767
+ `application` role. Its cells are `gridcell`s with one tab stop between them, and a screen reader
1768
+ reads each cell's value with its row and column headings. A calendar's cell reads its date first:
1769
+ "Sunday, October 5, 2025, 0" in Chromium. The readout over a cell is hidden from assistive
1770
+ technology, because the focused cell reads the same words.
1771
+ - The tooltip is an `output`, whose `status` role reads its whole text on a change. It is
1772
+ `aria-live="assertive"` while the keyboard layer is on, as recharts' own tooltip is, so a screen
1773
+ reader reads each point the arrows move to. In a pie the arrows move it from slice to slice.
1774
+ - The figure in a donut's or a sunburst's hole is hidden from assistive technology, because the
1775
+ caption states the finding.
1776
+ - The legend's buttons report `aria-pressed`, and each swatch is hidden from assistive technology.
1777
+ - Under forced colors neither Firefox nor Chromium replaces an SVG `fill` or `stroke`, so the series
1778
+ keep their colors. The tick labels, a `Label`'s text and the axis lines take `CanvasText`, the
1779
+ grid `GrayText`, and each swatch keeps its color inside a `CanvasText` edge. A quadrant chart's
1780
+ lines, its names and the words beside its points take `CanvasText`, and a timeline's markers keep
1781
+ their colors inside a `Canvas` edge. An annotation's words take `CanvasText` inside a `Canvas`
1782
+ halo. The crosshair takes `CanvasText`. A heatmap's cells keep their fills, a missing cell's
1783
+ dashed edge takes `GrayText`, and the lit headings take `Highlight`.
1784
+ - A preset animates its marks only with `animate`. In the kit, recharts animates marks unless the
1785
+ caller passes `isAnimationActive={false}`. recharts turns the animation off under reduced motion
1786
+ either way.
1787
+
1788
+ ### Not offered
1789
+
1790
+ - A data table behind the chart. The caption states the finding.
1791
+ - recharts' own `Legend`, which renders inside the plot and takes room from it.
1792
+ - Value labels on the cartesian presets' marks, and charts that share a tooltip through `syncId`.
1793
+ - recharts' `Brush`. Its two travellers are sliders valued in pixels, with no minimum, no maximum
1794
+ and one name for both. A range `Slider` zooms a preset instead.
1795
+ - A press on an annotation. A caption links the release or the incident an annotation marks.
1796
+
1797
+ ## Types
1798
+
1799
+ | Type | Describes |
1800
+ | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
1801
+ | `CartesianProps` | The props every preset takes |
1802
+ | `CartesianSeries` | A preset's series: the kit's series, `dashed` and `previousOf` |
1803
+ | `Annotation` | One mark on the categories: its key, words, color, category, period's end or value |
1804
+ | `AlignOptions` | How `alignPeriods` merges two periods: the category field, the copied fields and their suffix |
1805
+ | `BarSeries` | A bar chart's series: a preset's series with the field its `target` reads |
1806
+ | `BulletProps` | The props a bar chart adds: `zones` and `targetLabel` |
1807
+ | `ComboSeries` | A combo chart's series: a preset's series with its `mark` and `axis` |
1808
+ | `Mark` | The mark a series renders as: `area`, `bar` or `line` |
1809
+ | `ValueAxis` | The value axis a series reads: `start` or `end` |
1810
+ | `RangeBand` | A range chart's band: its key, label, color and its `low` and `high` field |
1811
+ | `Curve` | The path of a line between two points, `linear` or `monotone` |
1812
+ | `LineChartProps`, `AreaChartProps`, `StackedAreaChartProps` | A preset's props with its `curve`, and `percent` for stacked area |
1813
+ | `StepLineChartProps`, `StackedBarChartProps`, `PercentStackedBarProps` | A preset's props, which are `CartesianProps` |
1814
+ | `BarChartProps`, `HorizontalBarChartProps` | A bar chart's props: `CartesianProps` with `BarSeries` and `BulletProps` |
1815
+ | `ComboChartProps`, `RangeChartProps`, `ParetoChartProps`, `StreamGraphProps` | A preset's props with the props it adds |
1816
+ | `ParetoShares` | The `share` and `cumulative` share `paretoRows` adds to a row |
1817
+ | `BurndownChartProps`, `BurndownPoint`, `BurndownRow`, `BurndownOptions` | A burndown's props, one period, one row it plots, and its plan |
1818
+ | `WaterfallChartProps`, `WaterfallStep`, `WaterfallBar` | A waterfall's props, one step, and one bar it renders |
1819
+ | `ScatterPlotProps`, `ScatterSeries`, `BubbleChartProps` | A scatter's props, one series with its points, and a bubble's props |
1820
+ | `Quadrants`, `QuadrantId`, `Division` | A quadrant chart's names and where it divides, one quadrant's key, and the values it divides at |
1821
+ | `SpreadOptions`, `Span` | How `spreadPoints` spreads the points, and the two ends of an axis' span |
1822
+ | `TimelineChartProps`, `TimelineLane`, `TimelineEvent` | A timeline's props, one lane with its name, and one moment |
1823
+ | `EventLayout`, `EventLane`, `EventCluster`, `LayoutOptions` | The window and lanes `layoutEvents` returns, one lane, one marker's moments, and its options |
1824
+ | `RegressionOverlayProps`, `LinearFit` | A trend line's points, fields and series, and the fit `linearRegression` returns |
1825
+ | `HistogramChartProps`, `ChartBin`, `BinOptions` | A histogram's props, one bin with its count, and how `binValues` bins |
1826
+ | `DistributionChartProps`, `DistributionSeries` | A distribution chart's props, and one series with its values |
1827
+ | `BoxPlotProps`, `BoxGroup`, `BoxSummary` | A box plot's props, one group, and the summary `boxStats` returns |
1828
+ | `ViolinPlotProps`, `ViolinGroup`, `DensityPoint`, `DensityOptions` | A violin plot's props, one group, one point of a density, and how `kernelDensity` estimates it |
1829
+ | `CandlestickChartProps`, `Candle`, `CandleDirection`, `CandleChange` | A candlestick chart's props, one period's prices, its direction and its change |
1830
+ | `PieSlice` | One slice: its key, label, value and color |
1831
+ | `PolarProps`, `PieChartProps` | The props a pie takes |
1832
+ | `RadarChartProps`, `RadarSeries` | A radar chart's props, and one series with whether its polygon fills |
1833
+ | `RadialBarChartProps`, `RadialBarDatum` | A radial bar chart's props, and one bar with its key, label, value and color |
1834
+ | `PolarAreaChartProps` | A rose's props: the slices of a cycle, `max`, `color`, `names` and `values` |
1835
+ | `GaugeChartProps`, `GaugeZone`, `GaugeBand` | A gauge's props, one zone with where it ends, and one band `gaugeBands` returns |
1836
+ | `FunnelChartProps`, `FunnelStage`, `FunnelStep` | A funnel's props, one stage with its count, and one step `funnelSteps` returns |
1837
+ | `HierarchyProps`, `TreemapChartProps`, `HierarchyNode`, `HierarchyLeaf` | A hierarchy chart's props, a treemap's, one node, and one leaf `hierarchyLeaves` returns |
1838
+ | `SunburstChartProps` | A hierarchy chart's props with `center` and `centerLabel` |
1839
+ | `SankeyChartProps`, `SankeyNode`, `SankeyFlow`, `SankeyBalance` | A sankey's props, one node, one flow, and one node's balance `flowBalance` returns |
1840
+ | `ChordDiagramProps`, `ChordLayout`, `ChordSpan`, `ChordRibbon` | A chord diagram's props, and the arcs and ribbons `chordLayout` returns as spans of the circle |
1841
+ | `HeatmapProps`, `HeatmapCell`, `HeatmapHeading` | A heatmap's props, one reading, and the heading of a row, a column or a group of columns |
1842
+ | `HeatmapDomain`, `HeatmapScale`, `HeatmapColors`, `HeatShape`, `HeatSize` | A heat scale's span, how it reads a value, a diverging scale's colors, a cell's shape and size |
1843
+ | `Valued` | A reading `heatmapDomain` takes: anything with a `value` |
1844
+ | `CalendarDay`, `CalendarCell`, `CalendarOptions`, `Calendar` | One day's reading, one day as a cell, a calendar's window and words, and the props it returns |
1845
+ | `Cohort`, `CohortCell`, `CohortOptions`, `CohortGrid` | One intake, one cell of a cohort grid, the grid's words, and the props `cohortCells` returns |
1846
+ | `Retention`, `RetentionOptions`, `RetentionRow` | The props `retentionSeries` returns, its words and colors, and one row per period |
1847
+ | `DonutChartProps` | A pie's props with `center` and `centerLabel` |
1848
+ | `SparklineProps`, `SparkbarProps` | A spark's run, its color and shape, and the box's props |
1849
+ | `Chart.ChartOptions` | What `useChart` takes: the rows, the series, the hidden keys |
1850
+ | `Chart.ChartApi` | What `useChart` returns and `Chart.Root` provides |
1851
+ | `Chart.SeriesOptions` | A series the caller states |
1852
+ | `Chart.Series` | A series as the chart resolved it: key, label, color and opacity |
1853
+ | `Chart.ChartColor` | A hue palette, a semantic palette or a series color of the theme |
1854
+ | `Chart.TooltipEntry` | One value recharts passes the tooltip |
1855
+ | `Chart.TooltipRow` | One row `rowsOf` returns: its key, name and value |
1856
+ | `Chart.TooltipNote` | One note `notesOf` returns: its key, words and color |
1857
+
1858
+ Each part's props are `Chart.<Part>Props`.
1859
+
1860
+ ## Licence
1861
+
1862
+ MIT. See [LICENSE](LICENSE).