Chart primitives
The parts of a chart that are not the chart.
Why it exists
Seed, Branch and Bloom each grew a chart module — a burndown, a capture funnel, ninety days of a metric. Those are different charts, and Azalea does not try to draw them. What it replaces is the scaffolding all three built around them, which was the same every time.
Three copies, and two of them byte-identical
- Grid — the rules and their labels. Three copies; Branch's was the only one that thinned the lines when the range was small.
- Key — the legend swatch. Byte-identical in Seed and Branch.
- Empty — byte-identical in Seed and Branch; Bloom's differs only in its vertical padding.
- AXIS_TEXT = 11 — with the same paragraph of comment in all three explaining why the size is in viewBox units.
A chart built from them
The bars are the app's — one number per day and a colour for the part that failed. The frame, the grid, the legend and the axis maths are not.
The axis is a ruler
All three copies used the raw maximum, so a series topping out at 137 drew gridlines at 0, 68.5 and 137 — three labels, none of them a number anybody thinks in. niceMax rounds up to the next 1, 2 or 5 times a power of ten.
The second one is Branch's fix, kept: five gridlines over a maximum of two label as “0 1 1 2 2”, and an axis that repeats itself reads as a rendering bug — the reader stops trusting the numbers beside it.
Shape, not just colour
A legend of four identical rectangles in four colours is unreadable to anyone who cannot separate them — and every one of these charts uses colour to distinguish series. A dashed rule and a solid rule are still different in greyscale.
Empty says why
“No runs in this window” is a fact about the window. “No data” is indistinguishable from a broken query, and that is the moment somebody stops trusting the dashboard. It holds the chart's height so a panel with nothing in it does not reflow the page.
No runs in this window. Widen it, or check that the workflow has a trigger.
In both themes
light
dark
Guidance
Use it when
- Any chart an app draws itself. The scaffolding is shared even when the drawing is not.
- niceMax and gridFractions for the axis, always — they are the two decisions all three copies got wrong.
- A label on ChartFrame that states the shape, not the title: “peaking at 44 on Thursday” beats “Runs”.
Reach for something else when
- Promoting the chart itself. A burndown is Branch's and a funnel is Seed's; a shared library of one-app charts is how a design system fills up.
- Text inside the SVG for anything but axis labels. It does not wrap, does not select, and scales with the viewBox.
- Marking every rect up for a screen reader. The frame is one image with one name; sixty announced rectangles is worse than silence.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| ChartFrame width / height | number | — | The viewBox, not pixels. The svg is width: 100%. |
| ChartFrame label | string | — | Required. What the drawing shows, in a sentence. |
| ChartFrame caption | ReactNode | — | A figcaption — the legend, the window, the unit. |
| ChartGrid max | number | — | The axis top. Pass niceMax(values). |
| ChartGrid format | (value: number) => string | — | Formats the labels. Defaults to a rounded integer. |
| ChartGrid lines | number | 4 | Most gridlines. Thinned automatically for a small range. |
| LegendKey shape | "block" | "line" | "dashed" | "dot" | "block" | The swatch's form — the non-colour signal. |
| ChartEmpty height | number | — | Reserves the chart's height so the panel does not collapse. |
Why type is in viewBox units
An <svg> at width: 100% scales its whole coordinate system, so an 11px label inside a 680-unit chart renders at 11px only when the chart happens to be 680px wide. In a half-width card it comes out at six, which is not a font size, it is a texture. All three chart modules discovered this independently and each wrote the same paragraph about it; the paragraph is now in one file, and AXIS_TEXT is exported so a chart that needs its own labels uses the same number.