npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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-is

The 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 dashed renders its line in 6px dashes 4px apart, for a target or a projection. A series with previousOf renders 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 |

  • at and until are 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 Previous unless suffix states another, and the previous period's own category is copied too, as dayPrevious.
  • A series with previousOf renders dashed, in the neutral palette's chart color unless it states a color.
  • 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.
  • zones take the gauge's GaugeZone: where each zone ends, its palette and its name. They resolve against the value axis' ends, which valueDomain states, 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 targetLabel and lists each zone with a label.
  • 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 mark and axis. axis: "end" puts a series on a second value axis at the plot's end edge, which renders while a series reads it. endOptions writes its ticks and its series' tooltip values, and endDomain sets 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' BarChart renders the band cursor.
  • A range chart's band takes { 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. paretoRows returns the sorted rows with each row's share and running cumulative share, and paretoCutoff(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% unless threshold states otherwise, and 0 renders none.
  • A burndown chart takes { at, remaining } per period, with remaining missing for a period to come. The ideal line runs straight from total, else the first reading, to zero over periods, 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.
  • burndownFinish returns the period the projection falls to zero, as a fractional index, and burndownRows returns the rows the chart plots. The ideal line and the projection take fractional values. The burndown writes its values to one decimal unless valueOptions states otherwise.

Stream and waterfall

StreamGraph stacks many series about a moving baseline. Each band's rise and fall reads across a long span:

  • baseline is wiggle unless stated, or silhouette, 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. insideOutOrder and streamOnset return 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 value sets 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. waterfallBars returns the bars the chart renders, with each step's start, end and change.

| 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. xKey and yKey are the fields of a point.
  • xLabel and yLabel are 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, xDomain and yDomain format and range each axis.
  • A point is 60 square pixels unless size states otherwise. BubbleChart takes sizeKey and sizeLabel, and recharts scales each point's area over sizeRange, 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. defaultIndex opens 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.
  • RegressionOverlay is a recharts child that renders the least-squares line through data as 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's slope, intercept and r2. 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 a Scatter, fits a point without a value at pixel (0, 0), the top-left corner of the chart's svg, 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"
/>;
  • quadrants takes the name of each quadrant, keyed topStart, topEnd, bottomStart and bottomEnd in the reading direction, and x and y, 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.
  • labelKey names 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".
  • xEnds and yEnds name an axis' two ends, such as "Rare" and "Certain", in place of its ticks. The axis spans xDomain or yDomain, 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 than distance apart, 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.
  • bins aims for a number of bins. domain bins 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.
  • normalize counts 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's bins, valueOptions, grid, defaultIndex, animate, empty, locale, ratio and children, 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 a summary in 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's PERCENTILE_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 whisker interquartile 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. valueDomain sets 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. animate grows 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 at resolution even 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, unless bandwidth states 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 boxStats returns, 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 quartiles is 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, low and close. 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 is undefined for 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. labelOptions formats 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' snap125 rule.
  • 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.
  • maxSlices gathers the slices past it into one slice named by otherLabel, keyed other, 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.
  • DonutChart cuts a hole of 58% of the chart's radius and renders center and centerLabel in 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. valueDomain states 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. scale writes 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: false on 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 landscape unless 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.
  • max is 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 max closes its track, and the other rings keep their share of max. 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 subtle fill. track={false} leaves the tracks out. Under forced colors a track fills with Canvas inside a GrayText edge.
  • The rings turn clockwise from 12 o'clock. startAngle and endAngle take recharts' degrees, where 90 is 12 o'clock and clockwise is negative, so 225 and -45 turn 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 square unless 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 max the 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.
  • max is 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 past max extends 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 at max, 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 startAngle to endAngle, 225 to -45 in recharts' degrees unless stated: three quarters of a turn with the gap at the bottom.
  • The reading fills the dial from min in its zone's color, and the rest of the range renders as the track in the neutral palette's subtle fill. Under forced colors the track fills with Canvas inside a GrayText edge.
  • 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