| Hour | Served | Queued |
|---|---|---|
| 08:00 | 840 | 620 |
| 10:00 | 948 | 580 |
| 12:00 | 1,010 | 560 |
| 14:00 | 1,124 | 510 |
| 16:00 | 1,208 | 468 |
| 18:00 | 1,265 | 430 |
| 20:00 | 1,284 | 402 |
src/chart - time series and relationships
Charts, without a charting library
Six chart types and four shared pieces, in plain CSS and inline SVG you copy. Nothing here loads a library, nothing here needs a build step, and every one of them states its numbers as text as well as drawing them. The two behaviour modules add a legend filter and a measured axis, and every chart works without them.
Series are never told apart by colour alone. One attribute,
data-bs-series, sets an ink, a stroke pattern, a hatch
angle and a marker shape together, so a reader who cannot separate two hues separates a
dashed line with a square from a double line with a triangle. The palette is fixed by
the contract and five hues that all clear 3:1 in both themes do not exist in it, so
shape does the work colour cannot.
Line
An SVG polyline in a stretched viewBox, with the stroke weight and the dash pattern
pinned to device pixels by vector-effect so the same
chart draws one line weight in a rail and in a full-width panel. The sample points are
HTML rather than SVG circles, because
preserveAspectRatio="none" would stretch a circle into an
ellipse that changes shape with the container.
Area
The same geometry with the region under the curve closed and painted. The edge is always stroked: two translucent bands differ by a shade nobody can name, and where they cross there is a third shade that is not a series at all.
- 1 Jul
- 10 Jul
- 20 Jul
- 30 Jul
| Day | Data | Logs |
|---|---|---|
| 1 Jul | 41% | 18% |
| 10 Jul | 51% | 24% |
| 20 Jul | 70% | 32% |
| 30 Jul | 78% | 36% |
Scatter and bubble
Two measurements per observation, and a third in the area of the mark.
--bs-chart-scatter-r is a radius rather than a value: area
is what the eye reads in a circle, so a bubble worth twice as much is about 1.41 times
as wide. The size key states the scale in words, because nobody can estimate an area.
- 0
- 200
- 400
- 600
- 800
1,000 a month 9,000 a month
| Region | Requests | Latency | Spend |
|---|---|---|---|
| eu-west | 96 | 41 ms | 3,400 |
| eu-north | 192 | 48 ms | 5,100 |
| us-east | 280 | 63 ms | 2,800 |
| us-west | 352 | 76 ms | 7,900 |
| ap-south | 416 | 104 ms | 4,600 |
| ap-east | 488 | 116 ms | 6,200 |
| sa-east | 560 | 142 ms | 9,000 |
| af-south | 632 | 158 ms | 3,900 |
Heatmap
A real table, not a grid. A matrix with a label on every row and every column is tabular data, and a table is what gives it header association and a screen-reader table mode where a user hears "eu-west, 03, 14" with no ARIA written by hand. The intensity is capped at 70% of the accent, because the default ink over a full-strength accent measures 3.79:1 in the light theme and the digits in each cell have to stay readable.
| 00 | 04 | 08 | 12 | 16 | 20 | |
|---|---|---|---|---|---|---|
| eu-west | 3 | 9 | 22 | 37 | 17 | 5 |
| us-east | 12 | 7 | 11 | 28 | 33 | 18 |
| ap-south | 0 | No data | 15 | 20 | 9 | 2 |
0 37 errors
Calendar heatmap
A year of daily readings: seven rows, one column per week, written as a flat run that
grid-auto-flow: column folds into a calendar. Fifty-three
weeks is wider than a phone and no thinning helps, because dropping weeks changes the
data, so the plot scrolls inside its own box and the page never does. The key states
the numeric band of every step, which is where the original of this chart gives up.
None 0 1 to 3 4 to 7 8 to 15 16 and over
| Day | Deploys |
|---|---|
| Tue 3 Feb | 2 |
| Wed 4 Feb | 0 |
| Thu 5 Feb | 5 |
| Fri 6 Feb | 9 |
| Sat 7 Feb | 0 |
Candlestick
Four numbers in one mark. The body spans open to close and the wick spans low to high,
and CSS works out which end is the top with min() and
max(), so the template does no arithmetic beyond
normalising. An up session is hollow and a down session is filled: that convention
predates colour screens and it is why the direction survives a monochrome print, a
colour-blind reader and forced colors without a second encoding.
- 1 Apr
- 8 Apr
- 15 Apr
- 22 Apr
- 29 Apr
| Session | Open | High | Low | Close |
|---|---|---|---|---|
| 1 Apr | 141.20 | 148.60 | 132.10 | 146.90 |
| 2 Apr | 149.80 | 151.40 | 136.20 | 140.60 |
| 3 Apr | 142.10 | 153.90 | 137.50 | 151.20 |
| 29 Apr | 163.40 | 172.40 | 162.10 | 168.90 |
Axis, and the 320px problem
Seven time labels at caption size need about 250px of glyph plus the gaps. A 320px phone minus a gutter leaves about 288px, and a y axis takes 30 of those. Every library answers this by rotating, and rotation is the wrong default: it costs vertical space out of a plot that is already short, and CSS cannot then reflow the plot to pay for it. Narrow this page to a phone width and watch the three strategies below.
1. Thinning, the default. Minor ticks keep their mark and drop their label below 30em.
- 00:00
- 04:00
- 08:00
- 12:00
- 16:00
- 20:00
- 24:00
2. Stagger. Twice the labels at full legibility, for one extra line of axis height.
- 00:00
- 04:00
- 08:00
- 12:00
- 16:00
- 20:00
- 24:00
3. Rotate. Last resort, for labels that are genuinely long. It reserves the vertical room explicitly.
- 1 Feb 2026
- 1 Mar 2026
- 1 Apr 2026
- 1 May 2026
- 1 Jun 2026
Legend
The toggles in the line chart above are real buttons, so they are in the tab order, they answer to Enter and to Space, and a screen reader announces the pressed state. Tab to one and press it: the series leaves the plot, the swatch empties, the rule drains to the border colour and the label is struck through. Three signals and not one of them is hue.
- Solid, circle
- Dashed, square
- Dotted, diamond
- Double, triangle
- Dashed, ring
Empty, loading and one datum
A chart with one point is the state that ships broken most often, because a polyline through one point draws nothing at all. All three states below keep the plot box, so the page does not jump when the data arrives.
Empty
No requests in this window
Loading
One datum
One reading: 1,284 at 20:00.