Azalea

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.

SucceededFailed

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.

This periodPrevious period7-day meanDeployment

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

SucceededFailed

dark

SucceededFailed

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

PropTypeDefaultDescription
ChartFrame width / heightnumberThe viewBox, not pixels. The svg is width: 100%.
ChartFrame labelstringRequired. What the drawing shows, in a sentence.
ChartFrame captionReactNodeA figcaption — the legend, the window, the unit.
ChartGrid maxnumberThe axis top. Pass niceMax(values).
ChartGrid format(value: number) => stringFormats the labels. Defaults to a rounded integer.
ChartGrid linesnumber4Most gridlines. Thinned automatically for a small range.
LegendKey shape"block" | "line" | "dashed" | "dot""block"The swatch's form — the non-colour signal.
ChartEmpty heightnumberReserves 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.