@stealthscale/component-charts
v0.2.3
Published
Renders data as charts over recharts: a figure with a measured plot, a tooltip, a legend that switches series, and series colors from the theme.
Maintainers
Readme
@stealthscale/component-charts
@stealthscale/component-charts renders data as charts over recharts. The presets render line, area
and bar charts from declared series, bars and lines on two value axes, a forecast band, Pareto,
burndown, stream and waterfall charts, scatter and bubble charts with a trend line or four named
quadrants, histograms, distributions compared on shared bins, box plots and violins of groups,
candlesticks of prices, a pie or a donut from slices, a radar of dimensions, measures as rings
against a full turn, a rose of a cycle, one value on a dial against its zones, the stages of a
funnel with their rates, a hierarchy of parts as nested tiles or as rings, flows between stages as
bands, flows between peers as ribbons around a circle, a value for every pair of two sets of
categories as a grid of colored cells, moments on one time window across lanes, and a run of values
the size of a word. The presets are built on Chart, which renders a figure around recharts' own
chart with a measured plot, a tooltip, a legend that shows and hides series, a caption and an empty
state. A line, area or bar chart also marks moments, periods and points on its categories, and plots
an earlier period behind the current one. The heatmap renders an HTML grid in the figure in place of
a recharts chart. A caller who needs another chart writes it inside Chart. Each series takes its
color from the theme. The preset under ./theme registers the recipes with an application's
compiler.
Install
pnpm add @stealthscale/component-charts recharts react-isThe package depends on recharts 3.10.1, pinned exactly, because the recipe restyles the classes
recharts puts on its SVG. It depends on @stealthscale/component-data for ColorSwatch and on
@stealthscale/component-collections for the funnel's table and the heatmap's scroll area. It peers
on react, react-is, which recharts asks for, @stealthscale/component-primitives, whose scroll
area the table renders in, @stealthscale/theme, @stealthscale/hooks and
@stealthscale/provider-locale.
Presets
A preset takes the rows, the field the category axis reads and the series, and renders the whole chart:
import { BarChart } from "@stealthscale/component-charts";
<BarChart
caption="Growth sign-ups passed Starter in September, 158 to 152."
categoryKey="month"
data={months}
label="Sign-ups per plan per month"
labelOptions={{ month: "short", timeZone: "UTC" }}
series={[
{ key: "starter", label: "Starter" },
{ key: "growth", label: "Growth" },
]}
/>;| Preset | Renders | Own props |
| -------------------- | ---------------------------------------------------------- | ------------------------------------------ |
| LineChart | A line per series | curve |
| StepLineChart | A line per series, flat from each point to the next | |
| AreaChart | An area per series, each fading to the axis | curve |
| StackedAreaChart | The series stacked into one total | curve, percent |
| BarChart | A bar per series, side by side in each category | zones, targetLabel, a series' target |
| StackedBarChart | Each category's total as one bar, split into the series | |
| PercentStackedBar | Each category as one bar of 100%, split into shares | |
| HorizontalBarChart | A bar per series on its side, the categories down the edge | zones, targetLabel, a series' target |
curve is monotone unless stated, or linear for straight lines between the points. With
percent every point of a StackedAreaChart sums to 100%.
| Prop | Takes | Default |
| ------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------- |
| data | The rows, in order | Required |
| categoryKey | The field of each row the category axis reads | Required |
| series | { key, label, color, dashed, previousOf } per series, in legend order | Required |
| annotations | { key, at, label, color, until, value } per mark on the categories | None |
| label | The chart's accessible name, recharts' title | Required |
| caption | The finding in words, which names the figure | None |
| labelOptions | Intl.DateTimeFormatOptions for the category ticks and the tooltip's head | The category as it is |
| valueOptions | Intl.NumberFormatOptions for the value ticks and the tooltip's values | A number in the locale |
| valueDomain | recharts' domain of the value axis, such as [0.99, 1] | From zero |
| grid | Whether lines cross the plot at the value ticks | true |
| legend | Whether the legend renders | Two or more series, and rows |
| legendLabel | The legend's accessible name | Series |
| defaultIndex | The row the tooltip shows on the first render | None |
| animate | Whether the marks animate in, which they never do under reduced motion | false |
| empty | The message in the plot's place while the chart has no rows | No data |
| ratio | The plot's aspect ratio | video |
| hiddenKeys, defaultHiddenKeys, onHiddenKeysChange | The hidden series, controlled or from a first value | None hidden |
| locale | The locale the values are written in | The provider's, else the runtime's |
| children | recharts elements rendered after the marks, such as a ReferenceLine | None |
Any other prop goes to the figure.
Marks
- A line and an area's edge are 2px wide and have no dots. The point the tooltip is at takes a dot of radius 5 inside an edge in the panel's color.
- A series with
dashedrenders its line in 6px dashes 4px apart, for a target or a projection. A series withpreviousOfrenders dashed the same way. - An unstacked area fills with a gradient from 0.3 opacity at its top to transparent at the axis, so overlapping areas show through each other. A stacked area fills flat at 0.85 opacity.
- An unstacked bar rounds its end by 4px. A stacked bar keeps square ends, and a hairline edge in the panel's color parts the segments of a stack.
- In a stack summed to 100% the value ticks read in percent, and the tooltip writes each series' own value, not its share.
- The value axis labels every tick, and the grid renders a line at every label.
A reference line
A child renders inside the chart after the marks. Chart.colorOf returns the chart color of a
palette for its stroke, and the recipe sets its label in the chart's muted ink:
import { ReferenceLine } from "recharts";
import { Chart, LineChart } from "@stealthscale/component-charts";
<LineChart categoryKey="day" data={days} label="Cloud spend per day" series={[{ key: "spend" }]}>
<ReferenceLine
label={{ position: "insideTopRight", value: "Budget" }}
stroke={Chart.colorOf("error")}
strokeDasharray="6 4"
y={1200}
/>
</LineChart>;The recipe's muted ink applies over any fill a caller passes to a Label.
Annotations
annotations marks moments, periods and points on the category axis. The fields of each annotation
decide its mark:
import { LineChart } from "@stealthscale/component-charts";
<LineChart
annotations={[
{ at: "2026-09-17", key: "4.12", label: "Release 4.12" },
{ at: "2026-09-20", color: "info", key: "freeze", label: "Change freeze", until: "2026-09-22" },
{ at: "2026-09-18", color: "error", key: "outage", label: "Processor outage", value: 312 },
]}
categoryKey="day"
data={days}
label="p95 latency per day"
series={[{ key: "p95", label: "p95 latency" }]}
/>;| Fields | Mark |
| ---------------- | ------------------------------------------------------------------------------- |
| at | A moment: a rule across the plot in 4px dashes 3px apart |
| at and until | A period: a wash from at to until at 0.12 opacity, without an edge |
| at and value | A point: a ring of radius 5 around value at at, 2px wide and without a fill |
atanduntilare categories of the chart, in the values the category axis reads. A period's ends may come in either order.- A mark takes its annotation's
color, the neutral palette's chart color unless stated. - recharts renders a wash under the series and a rule and a ring over them. A ring widens the value axis to its value. On a chart of bars on their side the marks run along the category axis, down the plot.
- The words start 4px inside the top of a rule or a wash, or centre 4px above a ring, in the label ink inside a halo in the panel's color. Words that leave the plot's sides, or meet the words of an annotation earlier in the list, are hidden.
- The tooltip lists the annotations at its category after the series' rows, each with a swatch in its color. The arrows move the tooltip through every category, so the live region reads each annotation, hidden words included.
Two periods
alignPeriods(current, previous, options) returns a row per row of the current period, with the
fields of the previous period's row at the same position. A series with previousOf is the earlier
period of the series it names:
import { alignPeriods, LineChart } from "@stealthscale/component-charts";
const days = alignPeriods(thisWeek, lastWeek, { categoryKey: "day", keys: ["revenue"] });
<LineChart
categoryKey="day"
data={days}
label="Revenue per day"
series={[
{ key: "revenue", label: "This week" },
{ key: "revenuePrevious", label: "Last week", previousOf: "revenue" },
]}
/>;- The periods match by position, the first row against the first row. A previous period that is shorter leaves the last rows without its values.
- Each copied field takes the suffix
Previousunlesssuffixstates another, and the previous period's own category is copied too, asdayPrevious. - A series with
previousOfrenders dashed, in the neutral palette's chart color unless it states acolor. - The tooltip writes the change from the earlier period after the current value, as a signed percentage to one decimal after the locale's list separator: "€26,300, +8.2%". A row without a finite earlier value, or with an earlier value of zero, writes the value alone.
Zooming a long series
A preset plots the rows it receives, so a range picked in another control zooms it. The forms
Slider picks the range with two thumbs over the rows' indices. Each thumb takes its own label,
and getAriaValueText writes its date for a screen reader. The arrow keys, Page Up, Page Down, Home
and End move the focused thumb:
import { useState } from "react";
import { AreaChart } from "@stealthscale/component-charts";
import { Slider } from "@stealthscale/component-forms";
const [range, setRange] = useState([days.length - 91, days.length - 1]);
const [from = 0, to = 0] = range;
<>
<AreaChart
categoryKey="day"
data={days.slice(from, to + 1)}
label="Visitors per day"
series={[{ key: "visitors", label: "Visitors" }]}
/>
<Slider.Root
getAriaValueText={({ value }) => dateOf(value)}
max={days.length - 1}
minStepsBetweenThumbs={6}
onValueChange={({ value }) => {
setRange(value);
}}
value={range}
>
<Slider.Label>Days shown</Slider.Label>
<Slider.Control>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumb index={0} label="From" />
<Slider.Thumb index={1} label="To" />
</Slider.Control>
</Slider.Root>
</>;Targets and zones
A bar chart's series with target reads each row's target from that field and marks it with a tick
across the row's bar. zones fill each bar's row with the zones of the value axis. With both, each
bar is a bullet graph, and the rows compare their measures and their targets on one scale.
import { HorizontalBarChart } from "@stealthscale/component-charts";
<HorizontalBarChart
caption="Two of six teams passed their plan, and Borealis is behind at 64%."
categoryKey="team"
data={teams}
label="Plan attainment per team"
series={[{ key: "done", label: "Attainment", target: "plan" }]}
targetLabel="Plan"
valueDomain={[0, 1.2]}
valueOptions={{ style: "percent" }}
zones={[
{ color: "error", label: "Behind", upTo: 0.7 },
{ color: "warning", label: "Close", upTo: 0.9 },
{ color: "success", label: "On plan", upTo: 1.2 },
]}
/>;- The tick is 2px wide and 76% of the bar's thickness, in the ink inside a halo in the panel's color. A target past either end of the value axis is placed at that end. A row whose target is not a finite number renders no tick.
zonestake the gauge'sGaugeZone: where each zone ends, its palette and its name. They resolve against the value axis' ends, whichvalueDomainstates, and every row shares them.- Each zone fills the whole row, tinted at 40% of its palette over the panel, and the part of the axis no zone covers takes the track's fill. With zones the measure is 42% of the row's thickness, centred.
- The tooltip writes the value, then the target after
targetLabel, "Target" unless stated, then the name of the zone the value is in, where the name is text. - A key under the plot labels the tick with
targetLabeland lists each zone with alabel. - The zones and the tick keep their places while the bars animate in.
Keys
recharts' keyboard layer inverts the arrows in a sideways layout. ArrowLeft moves the tooltip down a
HorizontalBarChart, and ArrowRight moves it up.
Mixed marks
ComboChart, RangeChart, ParetoChart and BurndownChart render marks of different kinds on one
plot. They take the preset props, and each adds its own:
import { ComboChart } from "@stealthscale/component-charts";
<ComboChart
caption="Revenue rose to €254K in September at a 62% gross margin."
categoryKey="month"
data={months}
endDomain={[0, 1]}
endOptions={{ style: "percent" }}
label="Revenue and gross margin per month"
series={[
{ key: "revenue", label: "Revenue", mark: "bar" },
{ axis: "end", key: "margin", label: "Gross margin", mark: "line" },
]}
/>;| Preset | Renders | Own props |
| --------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| ComboChart | Each series as the mark it states: bar, line or area | curve, endDomain, endOptions |
| RangeChart | A band between two fields of each row, and lines plotted over it | band, curve |
| ParetoChart | Bars sorted largest first, and their running share on an end axis | valueKey, valueLabel, cumulativeLabel, threshold, thresholdLabel |
| BurndownChart | The work left per period, the ideal line and the projection | points, periods, total, projection, remainingLabel, idealLabel, projectedLabel |
- A combo series takes
markandaxis.axis: "end"puts a series on a second value axis at the plot's end edge, which renders while a series reads it.endOptionswrites its ticks and its series' tooltip values, andendDomainsets its range, from zero unless stated. - recharts renders areas behind bars and bars behind lines, whatever the series' order. A combo
chart's cursor is a line, because only recharts'
BarChartrenders the band cursor. - A range chart's
bandtakes{ key, label, color, low, high }. It fills the space between its two fields at 0.2 opacity with no edge, and the tooltip writes it as a range, such as "15,500–19,500". The low field must be below the high field in every row. - A Pareto chart sorts the rows largest first.
paretoRowsreturns the sorted rows with each row'sshareand runningcumulativeshare, andparetoCutoff(rows, threshold)counts the rows it takes to add up to the threshold, so a caption can state the count. The dashed threshold line is at 80% unlessthresholdstates otherwise, and0renders none. - A burndown chart takes
{ at, remaining }per period, withremainingmissing for a period to come. The ideal line runs straight fromtotal, else the first reading, to zero overperiods, else over the points. The projection continues the least-squares trend of the readings from the last reading and never goes below zero. A trend that does not fall has no projection. burndownFinishreturns the period the projection falls to zero, as a fractional index, andburndownRowsreturns the rows the chart plots. The ideal line and the projection take fractional values. The burndown writes its values to one decimal unlessvalueOptionsstates otherwise.
Stream and waterfall
StreamGraph stacks many series about a moving baseline. Each band's rise and fall reads across a
long span:
baselineiswiggleunless stated, orsilhouette, which centres the stack on a straight line. The graph renders no value axis and no grid, and the stack fills the plot's height.- The bands stack inside out. The series are sorted by onset, the centre of each series' mass, and
dealt onto the lighter of two piles, so a series that arrives late is outside and moves no band
beneath it.
insideOut={false}keeps the series' order, the first at the bottom. - The stack's order changes neither a series' color nor its place in the legend and the tooltip.
insideOutOrderandstreamOnsetreturn the order and the onsets. - The legend renders while the series fit the theme's eight series colors.
WaterfallChart renders the steps that take a running total from one number to another:
import { WaterfallChart } from "@stealthscale/component-charts";
<WaterfallChart
caption="MRR grew from €120K in August to €135.9K in September."
label="MRR bridge from August to September"
steps={[
{ key: "august", label: "August", total: true, value: 120_000 },
{ key: "new", label: "New", value: 18_400 },
{ key: "churn", label: "Churn", value: -7600 },
{ key: "september", label: "September", total: true },
]}
valueOptions={{ currency: "EUR", notation: "compact", style: "currency" }}
/>;- A change floats from the running total before it to the total after it, in the success palette's chart color when it rises and the error palette's when it falls. A total rises from zero in the neutral palette's chart color.
- A total with a
valuesets the running total, for an opening balance. A total without one shows the running total. - A dashed connector joins each bar to the next at the running total. The chart writes each bar's signed change or total above the bar and keeps 20px free above the tallest bar for it.
- The values show while the widest one fits a step, at 8px a character, and hide together in a narrower plot.
- The tooltip writes the same value as the bar, and its swatch takes the bar's color.
- The value axis rounds its range to whole ticks and runs through zero where the running total goes
below it.
waterfallBarsreturns the bars the chart renders, with each step'sstart,endandchange.
| Prop | Takes | Default |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------- |
| steps | { key, label, value, total } per step, in the order they happened | Required |
| label | The chart's accessible name, recharts' title | Required |
| caption | The finding in words, which names the figure | None |
| valueLabel | The name of the values in the tooltip | Amount |
| valueLabels | Whether each bar's value is written above it | true |
| connectors | Whether dashed connectors join the bars | true |
| valueOptions | Intl.NumberFormatOptions for the ticks, the tooltip and the bars | A number |
| grid, defaultIndex, animate, empty, locale, ratio, children | As the cartesian presets take them | |
Points
ScatterPlot renders a point per thing at two measures, and BubbleChart also sizes each point by
a third:
import { ScatterPlot } from "@stealthscale/component-charts";
<ScatterPlot
caption="Mid-market deals close within 50 days, while enterprise deals take 88 to 178 days."
label="Days to close by deal size"
series={[
{ key: "mid", label: "Mid-market", points: midMarket },
{ key: "enterprise", label: "Enterprise", points: enterprise },
]}
xKey="size"
xLabel="Deal size"
yKey="days"
yLabel="Days to close"
/>;- Each series takes its own
points, because a scatter's series do not share a category.xKeyandyKeyare the fields of a point. xLabelandyLabelare required titles, because neither axis of a scatter explains itself. The x title renders under the ticks and the y title along the start edge.xOptions,yOptions,xDomainandyDomainformat and range each axis.- A point is 60 square pixels unless
sizestates otherwise.BubbleCharttakessizeKeyandsizeLabel, and recharts scales each point's area oversizeRange, 40 to 1600 square pixels unless stated. Both axes are padded by the largest point's radius, so no point crosses the plot's edge. - The tooltip's heading is the label of the point's series. Each row writes a value in its axis'
format beside the axis' title.
defaultIndexopens it at a point of the first series. - recharts' keyboard layer walks the first series' points only, measured in Firefox and Chromium, so the series a reader walks goes first.
RegressionOverlayis a recharts child that renders the least-squares line throughdataas a dashed segment over the x range the points cover. The fit leaves out a point whose x or y is not a finite number.linearRegression(points, xKey, yKey)returns the line'sslope,interceptandr2. A caption states r², and a page leaves the line out where r² is low.- With
series, the line takes the series' color, hides while the legend hides the series, and fades with the series' points. Without it, the line takes the neutral palette's chart color and ignores the legend, for a fit across series. - recharts' own fitted line,
lineType="fitting"on aScatter, fits a point without a value at pixel (0, 0), the top-left corner of the chart'ssvg, and returns no r².
Quadrants
A quadrant chart divides the plot into four named quadrants and writes each point's name beside it:
import { ScatterPlot } from "@stealthscale/component-charts";
<ScatterPlot
caption="Alder and Cedar lead on both vision and execution."
label="Vendors by vision and execution"
labelKey="name"
quadrants={{
names: {
bottomEnd: "Visionaries",
bottomStart: "Niche players",
topEnd: "Leaders",
topStart: "Challengers",
},
}}
series={[{ key: "vendors", label: "Vendors", points: vendors }]}
xEnds={["Low", "High"]}
xKey="vision"
xLabel="Vision"
yEnds={["Low", "High"]}
yKey="execution"
yLabel="Execution"
/>;quadrantstakes the name of each quadrant, keyedtopStart,topEnd,bottomStartandbottomEndin the reading direction, andxandy, the values the plot divides at. Unless they are stated, the plot divides at the middle of each axis' span: the axis' domain where it states two numbers, 0 to 1 for an axis with named ends, else the points' own values.- Two dashed lines in the emphasized border ink divide the plot, and each name is written in its
corner in the muted ink. A point on a line belongs to the upper or the end side, and
quadrantOf(x, y, division)returns a point's quadrant. labelKeynames a field of each point whose words are written beside the point and head its tooltip. The words run toward the middle past 72% of the plot's width. Words that leave the plot, or meet the words of a point earlier in the series, are hidden, and the tooltip still names the point.- With
quadrants, the tooltip's heading names the point's quadrant after the point, joined by the locale's list separator: "Alder, Leaders". xEndsandyEndsname an axis' two ends, such as "Rare" and "Certain", in place of its ticks. The axis spansxDomainoryDomain, 0 to 1 unless it states two numbers, and the grid renders the plot's two edges alone along it.spreadPoints(points, options)moves points closer thandistanceapart, 0.06 of each axis' span unless stated, along a golden-angle spiral around each point's place. Each point keeps its quadrant, and every call places the points the same way. The function moves the plotted values, so the tooltip writes the moved values: it suits scores read by quadrant, never measurements.
Distributions
HistogramChart renders the shape of one distribution: its values counted into bins that touch
along a numeric axis.
import { HistogramChart } from "@stealthscale/component-charts";
<HistogramChart
caption="107 of 140 requests finish under 60ms, and a slow tail runs to 460ms."
countLabel="Requests"
label="Response times of the checkout API"
valueOptions={{ style: "unit", unit: "millisecond", unitDisplay: "narrow" }}
values={latencies}
/>;- The chart takes
values, one number per thing measured, not rows. A value that is not a finite number is left out. binCount(values)returns Freedman–Diaconis' count, or Sturges' where the middle half of the values ties, at most 30.binValues(values, { bins, domain })counts the values into bins whose 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 step coarser where the bins would pass 30.binsaims for a number of bins.domainbins a range from its lower edge and leaves out the values outside the bins. When every value is equal, the chart counts them in one bin a tenth of the value wide.- A bin runs from its lower edge up to its upper edge and leaves the upper edge to the next bin. The last bin includes its upper edge, so the largest value is counted.
- The bars touch, parted by the recipe's hairline in the panel's color. Each bar runs from its lower edge to its upper edge at the axis' own pixels, where recharts rounds a bar's width to a whole pixel.
- The axis labels every edge, else every second or every fifth, so at most eight even steps show. The tooltip heads each bar with its range, such as "20–40ms", and writes its count.
| Prop | Takes | Default |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------ |
| values | The values to count, one per thing measured | Required |
| label | The chart's accessible name, recharts' title | Required |
| caption | The finding in words, which names the figure | None |
| bins | The number of bins to aim for, at most 30 | Freedman–Diaconis' count |
| domain | The range to bin, from its lower edge | The values' range |
| countLabel | The name of the counts in the tooltip | Count |
| color | A hue or a palette for the bars | series.1 |
| valueOptions | Intl.NumberFormatOptions for the edges and the tooltip's heading | A number in the locale |
| grid, defaultIndex, animate, empty, locale, ratio, children | As the cartesian presets take them | |
DistributionChart compares two or more distributions on one set of bins:
import { DistributionChart } from "@stealthscale/component-charts";
<DistributionChart
caption="Three in four Free sessions end within 10 minutes, against one in ten Pro sessions."
label="Session lengths by plan"
normalize
series={[
{ key: "free", label: "Free", values: freeSessions },
{ key: "pro", label: "Pro", values: proSessions },
]}
valueOptions={{ style: "unit", unit: "minute", unitDisplay: "short" }}
/>;- The chart chooses the bins from every series' values together, so the series' bars start at the same edges. Histograms binned apart put their bars at different edges and widths.
normalizecounts each bin as a share of its own series' total, so series of different sizes compare. The value axis and the tooltip then write percents.- The bars of a bin are side by side in the series' order, each a share of the bin's width. A series the legend hides gives its share to the series shown.
- A series takes
{ key, label, color, values }. The chart takes the histogram'sbins,valueOptions,grid,defaultIndex,animate,empty,locale,ratioandchildren, and the cartesian presets' legend props.
BoxPlot compares the distributions of groups: each group's median, middle half, whiskers and
outliers.
import { BoxPlot } from "@stealthscale/component-charts";
<BoxPlot
caption="Singapore's median of 165ms is more than twice Frankfurt's 61ms."
countLabel="Requests"
groups={[
{ key: "frankfurt", label: "Frankfurt", values: frankfurt },
{ key: "singapore", label: "Singapore", values: singapore },
]}
label="Response times of the checkout API by region"
valueOptions={{ style: "unit", unit: "millisecond", unitDisplay: "narrow" }}
/>;- A group takes
{ key, label, values }, or asummaryin place of its values, for values summarised elsewhere.boxStats(values, whisker)returns the summary the chart renders.quantile(sorted, share)reads its quartiles by type 7, the rule of SQL'sPERCENTILE_CONT, so a query that summarises the values matches the chart. - The box runs from the first quartile to the third, and the median crosses it in the ink. Each
whisker extends to the furthest value within
whiskerinterquartile ranges of the box, and ends at the box's edge when no value is within that distance. - Every value past a whisker renders as a hollow point in the box's color, and outliers of equal
value share one point.
outliers={false}leaves the points out. - The value axis is rounded around the values, because a box plot compares where values fall.
valueDomainsets another range, such as one scale for every week. - A box is at most 72px wide. The box under the pointer or the keyboard fills at 0.6 of its color inside an edge 2px wide, and the rest fill at 0.35.
- The key under the plot labels the box, the median, the whiskers and the outliers beside a glyph of
each in the chart's inks, because a box plot is read by convention.
legend={false}leaves it out. - The tooltip heads a group with its name and writes its median, middle half, whiskers, outliers and
count.
animategrows each box from the axis with the bar's entrance.
| Prop | Takes | Default |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------- |
| groups | { key, label, values, summary } per group, in the axis' order | Required |
| label | The chart's accessible name, recharts' title | Required |
| caption | The finding in words, which names the figure | None |
| whisker | The whiskers' reach in interquartile ranges | 1.5 |
| outliers | Whether each outlier renders as a point | true |
| legend | Whether the key renders | true, while a group has values |
| color | A hue or a palette for the boxes | series.1 |
| valueDomain | recharts' domain of the value axis | Rounded around the values |
| valueOptions | Intl.NumberFormatOptions for the ticks and the tooltip's values | A number in the locale |
| medianLabel, quartilesLabel, whiskersLabel, outliersLabel, countLabel | The names in the tooltip and the key | Median, Middle half, Whiskers, Outliers, Count |
| grid, defaultIndex, animate, empty, locale, ratio, children | As the cartesian presets take them | |
ViolinPlot renders the density of each group's values, mirrored about the middle of its band, with
its quartiles inside.
import { ViolinPlot } from "@stealthscale/component-charts";
<ViolinPlot
caption="Search requests peak at 22ms and at 167ms."
countLabel="Requests"
groups={[
{ key: "search", label: "Search", values: search },
{ key: "checkout", label: "Checkout", values: checkout },
]}
label="Response times of the shop's API by endpoint"
valueOptions={{ style: "unit", unit: "millisecond", unitDisplay: "narrow" }}
/>;- A group takes
{ key, label, values }. The chart takes the values themselves, because no summary gives a density back. kernelDensity(values, { bandwidth, resolution })returns a Gaussian kernel density sampled atresolutioneven steps, 64 unless stated, from the smallest value to the largest. The outline stops at those values, because a kernel puts mass past the data, such as below zero for a duration.- Each group's kernel is as wide as
silvermanBandwidth(values)returns, unlessbandwidthstates one width for every group. Silverman's rule takes 0.9 times the smaller of the standard deviation and the interquartile range over 1.34, times the count to the power of −1/5. - A kernel wider than the gap between two peaks merges them, and a narrow one shows sampling noise as peaks.
- Each violin is as wide as its bar, 110px at most, at its own densest value. Its width compares within the group only.
- Inside each violin, a line runs between the whiskers
boxStatsreturns, a narrow bar in the ink spans the middle half, and a hollow point marks the median.quartiles={false}leaves the three out. - The violin under the pointer or the keyboard fills at 0.5 of its color inside an edge 2px wide, and the rest fill at 0.28.
densityPeaks(density)returns the values a density peaks at, each at least 5% of the tallest peak's height.- A violin's tooltip lists its median, middle half, range, peaks and count. Each peak takes the decimals of the density's sampling step, because a peak is one of the sampled values.
- The key labels the density. While
quartilesis on, it also labels the middle half and the median.
| Prop | Takes | Default |
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------- |
| groups | { key, label, values } per group, in the axis' order | Required |
| label | The chart's accessible name, recharts' title | Required |
| caption | The finding in words, which names the figure | None |
| bandwidth | The kernel's standard deviation for every group, in the values' units | Silverman's, per group |
| resolution | The number of points each density is sampled at | 64 |
| quartiles | Whether the line, the bar and the point render inside each violin | true |
| legend | Whether the key renders | true, while a group has values |
| color, valueDomain, valueOptions | As the box plot takes them | |
| medianLabel, quartilesLabel, rangeLabel, peaksLabel, countLabel, densityLabel | The names in the tooltip and the key | Median, Middle half, Range, Peaks, Count, Density |
| grid, defaultIndex, animate, empty, locale, ratio, children | As the cartesian presets take them | |
CandlestickChart renders each period's open, high, low and close as a body inside a wick:
import { CandlestickChart } from "@stealthscale/component-charts";
<CandlestickChart
caption="ACME rose 12.7% over September, from $156.22 to $176.03."
categoryKey="day"
data={days}
label="Daily prices of ACME"
labelOptions={{ day: "numeric", month: "short", timeZone: "UTC" }}
valueOptions={{ currency: "USD", style: "currency" }}
/>;- Each row takes
open,high,lowandclose. A row without four finite prices is left out. - The body runs from the open to the close, and the wick from the low to the high.
candleDirection(candle)compares the close with the candle's own open. A rising candle takes the success palette's chart color, a falling one the error palette's, and a flat one the neutral palette's.candleChange(candle)returns the change and its share of the open. The share isundefinedfor an open of 0.- A doji, a period that closed at its open, has a body 1.5px high. A candle is at most 28px wide, and the candle under the pointer or the keyboard takes a wick and an edge 2px wide.
- The category axis is ordinal, so a weekend takes no room.
labelOptionsformats the periods on the ticks and in the tooltip's heading. - The value axis is rounded around the prices and does not start at zero, because a price is read
for its movement. Its ticks step by 1, 2, 2.5 or 5 times a power of ten, recharts'
snap125rule. - A candle's tooltip lists its open, high, low and close, then the change with its sign and its percentage of the open to two decimals.
- The key labels the rising color and the falling color with the data package's
ColorSwatch. It labels the flat color while the chart has a flat candle, and then the body and the wick.
| Prop | Takes | Default |
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------- |
| data | The rows, each with open, high, low and close | Required |
| categoryKey | The field of each row the category axis reads | Required |
| label | The chart's accessible name, recharts' title | Required |
| caption | The finding in words, which names the figure | None |
| labelOptions | Intl.DateTimeFormatOptions for the category ticks and the tooltip's heading | The category as it is |
| valueOptions | Intl.NumberFormatOptions for the ticks, the prices and the change | A number in the locale |
| valueDomain | recharts' domain of the value axis | Rounded around the prices |
| legend | Whether the key renders | true, while a row has prices |
| openLabel, highLabel, lowLabel, closeLabel, changeLabel | The names in the tooltip | Open, High, Low, Close, Change |
| upLabel, downLabel, flatLabel, bodyLabel, wickLabel | The names in the key | Up, Down, Flat, Open to close, Low to high |
| grid, defaultIndex, animate, empty, locale, ratio, children | As the cartesian presets take them | |
Pie and donut
PieChart and DonutChart render the parts of one whole from slices:
import { DonutChart } from "@stealthscale/component-charts";
<DonutChart
caption="Video takes 60% of the 195.5 GB in use."
center="195.5 GB"
centerLabel="in use"
label="Storage per kind of file"
slices={[
{ key: "video", label: "Video", value: 118 },
{ key: "images", label: "Images", value: 42 },
{ key: "archives", label: "Archives", value: 26 },
]}
/>;- The chart renders the slices largest first, from 12 o'clock and clockwise, because a reader compares each slice with its neighbours.
- The chart writes each slice's share of the slices shown on the slice, in the chart's locale, in
the ink inside a halo in the panel's color, which reads on a slice of any color. The pointer
passes through a share to its slice.
shares={false}leaves the shares out. maxSlicesgathers the slices past it into one slice named byotherLabel, keyedother, in the neutral palette's chart color.- A slice the legend hides leaves the pie, and the shares are of the slices shown. The pointer or focus on a legend button fades every other slice.
DonutChartcuts a hole of 58% of the chart's radius and renderscenterandcenterLabelin it. The chart does not sum the slices, because the figure in the hole is as often the largest share or a change as the total.
| Prop | Takes | Default |
| -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -------------------------- |
| slices | { key, label, value, color } per slice, in any order | Required |
| label | The chart's accessible name, recharts' title | Required |
| caption | The finding in words, which names the figure | None |
| shares | Whether each slice's share is written on it | true |
| maxSlices | The largest number of slices the pie renders | Every slice |
| otherLabel | The name of the slice the tail gathers into | Other |
| valueOptions | Intl.NumberFormatOptions for the tooltip's values | A number in the locale |
| legend | Whether the legend renders | true, while slices exist |
| ratio | The plot's aspect ratio | square |
| legendLabel, defaultIndex, animate, empty, hiddenKeys, defaultHiddenKeys, onHiddenKeysChange, locale, children | As the cartesian presets take them | |
| center, centerLabel | DonutChart only: the figure in the hole and its words | None |
Radial charts
RadarChart renders each series' values across three or more dimensions as a polygon on one spoke
per dimension:
import { RadarChart } from "@stealthscale/component-charts";
<RadarChart
caption="Cost is the only score that fell this quarter, from 6 to 4."
categoryKey="dimension"
data={scores}
label="Checkout service scores out of 10"
series={[
{ key: "current", label: "This quarter" },
{ key: "previous", label: "Last quarter" },
]}
valueDomain={[0, 10]}
/>;- Each row is a spoke named by
categoryKey, and each series is a polygon. The chart takes the cartesian presets' props. - The spokes share one radius axis, so the dimensions must share a scale.
valueDomainstates it, such as[0, 10]for scores out of ten. recharts rounds the axis from zero around the values unless it is stated. - The web's rings follow six radius ticks, which step a top of 1, 5, 10 or 100 evenly.
scalewrites the ticks up the spoke at 12 o'clock, each hanging from its ring inside a halo in the panel's color. - A polygon fills at 0.25 of its color inside an edge 2px wide.
filled: falseon a series leaves the outline, for three series or more. - The web's radius is 72% of the largest circle the plot fits, which leaves the spokes' names room
beside it. The plot's ratio is
landscapeunless stated. - The polygon's shape follows the order of the rows, so two orders of the same scores render two shapes. Compare radars only with their spokes in one order.
- The tooltip's heading is the spoke's name, with a row per series. The arrows walk the spokes, and the cursor is a line along the spoke under the pointer.
| Prop | Takes | Default |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------- |
| series | { key, label, color, filled } per series, in the legend's order | Required |
| scale | Whether the radius ticks are written up the top spoke | false |
| ratio | The plot's aspect ratio | landscape |
| data, categoryKey, label, caption, labelOptions, valueOptions, valueDomain, grid, legend, legendLabel, defaultIndex, animate, empty, the hidden keys, locale, children | As the cartesian presets take them | |
RadialBarChart renders each measure as a ring read against the value of a full turn, the first
ring outermost:
import { RadialBarChart } from "@stealthscale/component-charts";
<RadialBarChart
bars={[
{ key: "storage", label: "Storage", value: 0.82 },
{ key: "seats", label: "Seats", value: 0.61 },
{ key: "api", label: "API calls", value: 0.34 },
]}
caption="Storage is at 82% of the plan's quota, and nothing else is past two thirds."
label="Plan usage"
max={1}
valueOptions={{ style: "percent" }}
/>;- A ring shows one measure against its own track, such as the use of a quota. A bar chart compares measures with each other.
maxis the value of a full turn, such as a quota, a target or a capacity. Without it the largest value shown closes its track, and every other ring is measured against that value.- A ring past
maxcloses its track, and the other rings keep their share ofmax. A ring below zero renders no arc. The legend and the tooltip write each ring's own value. - A ring's length is its angle times its radius, so the same value renders a longer arc further out.
The legend writes each ring's value beside its name, because an arc is not read for a quantity.
values={false}leaves the names alone. - Each ring's track is the rest of its full turn in the neutral palette's
subtlefill.track={false}leaves the tracks out. Under forced colors a track fills withCanvasinside aGrayTextedge. - The rings turn clockwise from 12 o'clock.
startAngleandendAngletake recharts' degrees, where 90 is 12 o'clock and clockwise is negative, so225and-45turn three quarters with the gap at the bottom. - The hole in the middle is 30% of the largest circle the plot fits. The plot's ratio is
squareunless stated. - The tooltip writes the ring's name and value without a heading, and no cursor renders. The arrows walk the rings from the outermost.
- A ring the legend hides leaves the chart. With
maxthe other rings keep their turn.
| Prop | Takes | Default |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------ |
| bars | { key, label, value, color } per ring, the outermost first | Required |
| label | The chart's accessible name, recharts' title | Required |
| caption | The finding in words, which names the figure | None |
| max | The value of a full turn | The largest value shown |
| track | Whether the rest of each ring's turn renders behind it | true |
| values | Whether the legend writes each ring's value beside its name | true |
| valueOptions | Intl.NumberFormatOptions for the legend's and the tooltip's values | A number in the locale |
| startAngle, endAngle | The angles the rings start and end at, in recharts' degrees | 90, -270 |
| legend | Whether the legend renders | true, while bars exist |
| ratio | The plot's aspect ratio | square |
| legendLabel, defaultIndex, animate, empty, the hidden keys, locale, children | As the cartesian presets take them | |
PolarAreaChart renders a rose: each category of a cycle as a wedge of the same angle, whose area
follows its value:
import { PolarAreaChart } from "@stealthscale/component-charts";
<PolarAreaChart
caption="The load builds from 06:00, peaks at noon and falls by 21:00."
label="Requests a minute by hour"
slices={[
{ key: "00", label: "00:00", value: 120 },
{ key: "06", label: "06:00", value: 210 },
{ key: "12", label: "12:00", value: 1180 },
{ key: "18", label: "18:00", value: 640 },
]}
/>;- The wedges run clockwise from 12 o'clock in the slices' order, so twelve months read like a clock face. A rose suits categories that wrap, such as hours, months or compass points. A bar chart compares values on their own.
- A wedge's radius is √(value /
max) of the full radius, so its area follows its value. A wedge worth four times another is twice as long and covers four times the area, where a radius in proportion to the value would cover sixteen times.areaRadius(value, max)returns the share of the full radius. maxis the value of a full radius, such as a capacity. Without it the wedge of the largest value shown extends to the full radius. A wedge pastmaxextends to the full radius too, and a value below zero renders no wedge.- Every wedge takes
color, the theme's first series color unless stated, or the palette its slice states. The fill rises from 0.45 of the color at zero to the whole color atmax, so the shade repeats the radius. - The chart writes the names on one circle around the rose, each centred on its wedge. Names of five
characters, such as 00:00, clear a room 20rem wide by 10px.
names={false}leaves them out, and the wedges fill the whole circle. - The legend writes each value beside its name, because a radius is not read for a quantity.
values={false}leaves the names alone. - A wedge the legend hides keeps its place with no radius, so no category moves into another's place. The other wedges are measured against the largest value shown.
- The tooltip writes the wedge's name and value without a heading. The arrows walk the wedges clockwise from 12 o'clock.
| Prop | Takes | Default |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------- |
| slices | { key, label, value, color } per category, in the cycle's order | Required |
| label | The chart's accessible name, recharts' title | Required |
| caption | The finding in words, which names the figure | None |
| max | The value of a full radius | The largest value shown |
| color | A hue, a palette or a series color for every wedge | series.1 |
| names | Whether the names render around the rose | true |
| values | Whether the legend writes each value beside its name | true |
| valueOptions | Intl.NumberFormatOptions for the legend's and the tooltip's values | A number in the locale |
| legend | Whether the legend renders | true, while slices exist |
| ratio | The plot's aspect ratio | square |
| legendLabel, defaultIndex, animate, empty, the hidden keys, locale, children | As the cartesian presets take them | |
GaugeChart renders one value on a dial from a range's minimum to its maximum, with the zones that
name what the value means:
import { GaugeChart } from "@stealthscale/component-charts";
<GaugeChart
caption="p99 latency is 740ms, inside the 900ms error budget."
label="Checkout p99 latency"
max={1200}
value={740}
valueOptions={{ style: "unit", unit: "millisecond", unitDisplay: "narrow" }}
zones={[
{ color: "success", label: "Healthy", upTo: 600 },
{ color: "warning", label: "Watch", upTo: 900 },
{ color: "error", label: "Critical", upTo: 1200 },
]}
/>;- A gauge shows where one value is in a range, such as a latency against its error budget. A bar chart compares values with each other.
- The dial turns clockwise from
startAngletoendAngle,225to-45in recharts' degrees unless stated: three quarters of a turn with the gap at the bottom. - The reading fills the dial from
minin its zone's color, and the rest of the range renders as the track in the neutral palette'ssubtlefill. Under forced colors the track fills withCanvasinside aGrayTextedge. - The chart writes the value in the middle of the dial and the zone's name under it, because an arc
is not read for a quantity.
centerLabel
