PyramidChart

Two series mirrored about a centre, on one shared scale.

Two series drawn as mirrored bars either side of a centre line, one row per category.

Both wings are measured on the same scale, derived from the larger of the two series. That is the whole point of the shape: a bar twice as long as the one facing it means twice as much. Colours come from the --color-chart-1--color-chart-5 tokens.

For two series compared side by side rather than back to back, use BarChart.

Installation

PyramidChart ships with the library — no separate install.

import { PyramidChart, Frame, Text } from 'panelui-native';

Or copy the source into your project, to own and edit it:

npx panelui-cli@latest add pyramid-chart

Usage

<PyramidChart data={penguins} xDataKey="species">
  <PyramidChart.Grid />
  <PyramidChart.Bar dataKey="male" side="start" />
  <PyramidChart.Bar dataKey="female" side="end" colorIndex={5} />
  <PyramidChart.XAxis />
  <PyramidChart.YAxis />
  <PyramidChart.Tooltip />
</PyramidChart>

Composition

<PyramidChart>
  <PyramidChart.Grid />                        {/* mirrored lines, and the centre */}
  <PyramidChart.Bar dataKey="…" side="start" /> {/* the left wing */}
  <PyramidChart.Bar dataKey="…" side="end" />   {/* the right one */}
  <PyramidChart.Skeleton />                    {/* while status="loading" */}
  <PyramidChart.XAxis />                       {/* values, mirrored around zero */}
  <PyramidChart.YAxis />                       {/* the category names */}
  <PyramidChart.Legend />
  <PyramidChart.Tooltip />                     {/* the drag, and the readout */}
</PyramidChart>
  • PyramidChart.Header — See props below.
  • PyramidChart.Grid — Mirrored reference lines, plus the solid line down the middle both wings are measured from.
  • PyramidChart.Bar — One wing. side decides which, and the default colour differs per side so two bars declared with nothing but a key and a side are already told apart.
  • PyramidChart.Skeleton — Equal stubs either side of the centre, shown while status="loading".
  • PyramidChart.XAxis — The value labels along the bottom — the same magnitudes twice, either side of a zero in the middle.
  • PyramidChart.YAxis — The category names, one per row. By default each sits on its own line over the pair of bars it belongs to; labelPlacement moves them into a gutter between the wings or down the left instead.
  • PyramidChart.Tooltip — The drag over the plot, the row it highlights, and the readout that follows it down.
  • PyramidChart.Legend — A swatch and a name per series.

Examples

Two wings on one scale

side puts a series on the left or the right. The scale is shared, so the two halves can be read against each other rather than only each against itself.

<Frame className="w-full">
  <Frame.Header>
    <Frame.Title>Penguins observed</Frame.Title>
    <Frame.Action>Drag to inspect</Frame.Action>
  </Frame.Header>
  <Frame.Panel>
    <PyramidChart data={PENGUINS} xDataKey="species" aspectRatio={1.4}>
      <PyramidChart.Header
        value="334"
        caption="Three species"
        labels={{ male: 'Male', female: 'Female' }}
        legend
      />
      <PyramidChart.Grid />
      <PyramidChart.Bar dataKey="male" side="start" />
      <PyramidChart.Bar dataKey="female" side="end" colorIndex={5} />
      <PyramidChart.XAxis />
      <PyramidChart.YAxis />
      <PyramidChart.Tooltip />
    </PyramidChart>
  </Frame.Panel>
</Frame>

Names between the wings, or down the left

labelPlacement decides where the category names go. The default gives each row a line of its own over its pair of bars. center puts them in a gutter between the wings instead — taken off the bars rather than off the edges, so both wings stay equal — and start puts them down the left, for a chart with room on that side.

Height is the chart's width over aspectRatio and has nothing to do with how many rows there are. So a stack that is running past the bottom of the screen is shortened by raising aspectRatio, not by dropping rows — dropping rows only makes the bars thicker in a frame exactly as tall as before.

<PyramidChart
  data={POPULATION}
  xDataKey="band"
  labelPlacement="center"
  aspectRatio={1.5}
  barGap={0.3}
>
  <PyramidChart.Grid columns={2} />
  <PyramidChart.Bar dataKey="men" side="start" />
  <PyramidChart.Bar dataKey="women" side="end" colorIndex={5} />
  <PyramidChart.XAxis />
  <PyramidChart.YAxis />
  <PyramidChart.Tooltip />
</PyramidChart>

A readout that follows the finger

onActiveIndexChange fires when the row under the finger changes, not on every frame. The header's value is passed in rather than derived, so one header can show a total when nothing is pressed and a row's figures when something is.

const [active, setActive] = useState(null);

return (
  <PyramidChart
    data={PENGUINS}
    xDataKey="species"
    onActiveIndexChange={(_index, datum) => setActive(datum)}
  >
    <PyramidChart.Header
      value={String(active ? active.male + active.female : 334)}
      caption={active ? `${active.species} · both wings` : 'Three species'}
      legend
    />
    <PyramidChart.Grid />
    <PyramidChart.Bar dataKey="male" side="start" />
    <PyramidChart.Bar dataKey="female" side="end" colorIndex={5} />
    <PyramidChart.Tooltip />
  </PyramidChart>
);

Loading

status="loading" holds the bars at the centre and grows them out into the real ones when it turns ready. The placeholder wings are equal on purpose — unequal ones are a distribution the reader cannot tell from the real one until it changes under them.

<PyramidChart data={PENGUINS} xDataKey="species" status={status}>
  <PyramidChart.Grid />
  <PyramidChart.Skeleton />
  <PyramidChart.Bar dataKey="male" side="start" />
  <PyramidChart.Bar dataKey="female" side="end" colorIndex={5} />
  <PyramidChart.XAxis />
  <PyramidChart.YAxis />
</PyramidChart>

API Reference

PyramidChart

PropTypeDefaultDescription
classNamestring
dataPyramidChartDatum[]The rows. Each one is a band across the chart, with a wing either side.
xDataKeystring'name'Key holding the category label. Used by the axis and the readout.
statusPyramidChartStatus'ready'loading holds the bars at the centre and grows them out into the real ones when it turns ready. One component throughout, rather than a spinner swapped for a chart — swapping loses the transition. Add a PyramidChart.Skeleton for something to stand in the plot meanwhile.
aspectRationumber1.2Width ÷ height. 1.2 suits three or four rows in a card.
animationDurationnumber700Milliseconds for the bars to grow out on mount.
domainDurationnumber500Milliseconds for the scale to settle after the data changes.
maxValuenumberFix the far end of the shared scale instead of deriving it. The near end is zero either way — a pyramid measures outward from its centre.
labelPlacementPyramidChartLabelPlacement'above'Where the category names sit. above, the default, gives each row a line of its own over its pair of bars, so the two wings meet in the middle with nothing standing between them. center puts the names in a gutter between the wings instead, and start down the left edge.
barGapnumber0.25Fraction of each band left empty, 0 to 1. A fraction rather than a pixel gap so the proportions hold at any height.
barWidthnumberFixed bar thickness in points. Derived from the band when omitted.
cornerRadiusnumber4Corner radius on the outward end of a bar.
minBarLengthnumber0Smallest length a non-zero bar is drawn at, in points. A value that rounds to nothing still happened, and a bar of zero length says it did not.
fadedOpacitynumber0.3Opacity of the rows that are not under the finger.
onActiveIndexChange(index: number, datum: PyramidChartDatum | null) => voidThe row under the finger as it moves, and -1/null when it lifts. Fires when the index changes, not per frame.

PyramidChart.Grid

PropTypeDefaultDescription
columnsnumber2How many lines to draw per wing, not counting the centre.
colorstring
dashArraystring
opacitynumber1
centreLinebooleantrueDraw the solid line down the middle the wings are measured from.

PyramidChart.Bar

PropTypeDefaultDescription
dataKeystringColumn in the data holding this series' values.
sidePyramidChartSide'end'Which wing it grows into.
colorstringExplicit colour. Defaults to the --color-chart-* token for colorIndex.
colorIndexSeriesColorIndexWhich of the five chart tokens to take. Defaults to a different one per side, so two bars declared with nothing but a dataKey and a side are already told apart.
cornerRadiusnumberCorner radius, overriding the chart's.

PyramidChart.Skeleton

PropTypeDefaultDescription
rowsnumberHow many placeholder rows to draw. Defaults to one per row of data, and to five when the data has not arrived — the count is the one thing a loading chart can be honest about only if it already has the rows.
durationnumberMilliseconds for one pass of the sweep.
colorstring

PyramidChart.XAxis

PropTypeDefaultDescription
ticksnumberHow many labels per wing, not counting the zero in the middle.
format(value: number) => stringFormat a value for its label. Defaults to a compact number.
classNamestring

PyramidChart.YAxis

PropTypeDefaultDescription
format(datum: PyramidChartDatum, index: number) => stringTurn a row into its label. Defaults to the value at xDataKey.
classNamestring

PyramidChart.Tooltip

PropTypeDefaultDescription
formatValue(value: number, key: string) => stringFormat one series' value. Defaults to a compact number.
formatX(datum: PyramidChartDatum) => stringFormat the readout's heading from the row. Defaults to the value at xDataKey.
classNamestring

PyramidChart.Legend

PropTypeDefaultDescription
classNamestring
labelsRecord<string, string>Prettier names for the series keys.

PyramidChart.Header

PropTypeDefaultDescription
classNamestring
titlestringSmall line above the value — what the chart is of.
valuestringThe readout. The largest thing on the card, and the first thing read.
captionstringOne muted line under the value — a period, a comparison, a total.
labelsRecord<string, string>Prettier names for the series keys, as the legend takes.
legendbooleanfalseDraw a swatch and a name per series along the trailing edge. Prefer this to PyramidChart.Legend on a chart that has a header: the legend floats over the plot, where it competes with the bars for the same corner.

Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.

Notes

One scale, and where it comes from

The far end of the scale is the largest value in either series, with a little headroom. The near end is zero and cannot be moved — a wing cropped at its base is a length that lies. maxValue fixes the far end when several charts have to be comparable with each other.

Which side a series is on comes from side, not from the sign of its numbers, so a value is a distance outward from the centre. A negative one has no direction left to grow in and is drawn as nothing; it still appears in the readout, so a data error shows up as a gap rather than as a bar pointing the wrong way.

Where the names go

labelPlacement="above", the default, gives each row a line of its own over its pair of bars, so the two wings meet in the middle with nothing standing between them and the name is read before the lengths it belongs to. "center" puts the names in a gutter between the wings instead — taken off the bars rather than off the edges, so both wings stay equal — and "start" puts them down the left.

The value labels along the bottom are held inside the chart: the outermost tick of each wing sits on the plot's own edge, and a label centred there would hang half its width off the side.

Drawing

Each wing is one animated path split in two — the row under the finger, and everything else — so a chart of thirty rows costs two animated props a frame rather than thirty. The corners are rounded on the outward end only, which is why they are drawn as a path rather than with rx.

The axis labels are React Native text over the plot rather than SVG text, so they follow the theme's font and the platform's text scaling.

Accessible data

The chart exposes one screen-reader summary and one semantic entry per data row; its SVG paths, axes and markers stay decorative. Set accessibilityLabel for the summary, accessibilityLabelForDatum to phrase a row in your own words, and onAccessibilityDatumPress to make each row activatable. Pass accessible={false} to drop the semantic layer entirely.

Public exports

Values: PyramidChart, usePyramidChart

Types: PyramidChartProps, PyramidChartHandle, PyramidChartHeaderProps, PyramidChartGridProps, PyramidChartBarProps, PyramidChartSkeletonProps, PyramidChartXAxisProps, PyramidChartYAxisProps, PyramidChartTooltipProps, PyramidChartLegendProps, PyramidChartDatum, PyramidChartStatus, PyramidChartSide, PyramidChartLabelPlacement

On this page