Skip to content
microcharts
ReferenceExpressiveTreeRings

TreeRings

React tree rings chart — per-period growth as concentric rings, oldest at the center. Tiny accessible SVG microchart; RSC-safe static or /interactive.

encodes
radial ring thickness ∝ per-period value
precision
medium
nodes
≤ 4 (merged ring path + highlighted ring + centre dot + label)

TreeRings answers "how did growth accumulate, period over period?" like the rings of a tree: the oldest period is at the centre and each new period adds a ring outward, whose thickness is that period's value. The channel is thickness, not area — equal thickness at a larger radius spans more area (the ring illusion), so read the thicknesses, not the wedges; exact per-period reads steer to SparkBar.

TreeRings
interactive · 4.61 kB · static · 2.2 kB

Install

Import & usereact/tree-rings
import { TreeRings } from "@microcharts/react/tree-rings";// years — real values under “Sample data” below<TreeRings data={years} unit="years" periodWord="year" title="Account age" />
Sample data
const years = [8, 12, 10, 18, 22, 15, 20, 14];
Needs package + stylesheetSet up with AI

Try it

When to use it

  • Good for — account or company age at a glance, a cohort-age marker in a table cell, or a per-period growth story in a KPI card.
  • Avoid for — exact per-period reads (SparkBar), many periods (over twenty-four), or non-cumulative series.

Sizing

Variants

filled annuli + cohort age (total)
locale — the last-value label and the summary both follow it

Edge cases

a zero-value period collapses cleanly, it isn't padded to a minimum ring
no data yet

Four homes

Why this default

Stroke rings are the default because at twenty-four pixels hairlines keep the disc quiet and let the accent ring — the current period, drawn 1.5× heavier in the accent color (weight and color, never color alone) — stay loud. The channel is radial thickness, and the docs say so explicitly, because equal thickness at a larger radius spans more area. TreeRings never enforces a minimum visual thickness: a near-zero period looks near-zero, and a zero-value period collapses its two boundaries onto each other. Pass total for the cohort-age story — the disc then fills only Σdata/total of the radius, so a young account reads as part-grown.

Accessibility

The accessible name summarizes the growth — "8 periods; latest 14, biggest 22 in period 5." The interactive entry steps the rings from the centre out with ←/→ (or hover), announcing each period as "Year 5: 22."

The interactive entry follows the shared interaction contract: arrow keys rove between units on both axes, Home and End jump to the ends, and a click, tap, Enter or Space selects a unit — pinning its readout so it survives blur, until you select it again or press Escape. On touch, a tap pins and a drag scrubs.

Props

PropTypeNotes
data*number[]Per-period growth, oldest first.
highlight"last" | "none" | numberWhich period's ring to pick out.
totalnumberExpected lifetime Σ — the disc fills only Σdata/total.
rings"stroke" | "fill"Boundary rings (default) or filled annuli.
periodWordstringSingular period noun for the summary (default "period").
unitstringPlural period noun for the summary (default "periods").
sizenumberRings box edge in viewBox units (default 24).
fontSizenumberType size of the gutter label, in viewBox units. Defaults from `size`.
animatebooleaninteractiveOpt-in entrance motion when the chart mounts client-side — add import "@microcharts/react/motion" once. Inert on the server, on hydrated server HTML, and under prefers-reduced-motion.

Plus the shared grammar — data, domain, color, title, summary, format — and the layout props (width, height, className, style) that every chart accepts. Interactive entries also share animate and live, and — wherever a chart has more than one navigable unit — onActive, onSelect, selectedIndex and defaultSelectedIndex; and — wherever the chart shows a hover value — readout. See the shared grammar.

Related charts