Skip to content

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.

src/chart/line.css
Requests per second 840 low, 1,284 high, 1,284 now
  1. 08:00 840
  2. 10:00 948
  3. 12:00 1,010
  4. 14:00 1,124
  5. 16:00 1,208
  6. 18:00 1,265
  7. 20:00 1,284
  1. 08:00 620
  2. 10:00 580
  3. 12:00 560
  4. 14:00 510
  5. 16:00 468
  6. 18:00 430
  7. 20:00 402
Requests per second, every two hours
HourServedQueued
08:00840620
10:00948580
12:001,010560
14:001,124510
16:001,208468
18:001,265430
20:001,284402

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.

src/chart/area.css
Disk used 41% to 78% over 30 days
Disk used, percent of capacity
DayDataLogs
1 Jul41%18%
10 Jul51%24%
20 Jul70%32%
30 Jul78%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.

src/chart/scatter.css
Latency against throughput 12 regions, bubble is monthly spend

1,000 a month 9,000 a month

Median latency against throughput, by region
RegionRequestsLatencySpend
eu-west9641 ms3,400
eu-north19248 ms5,100
us-east28063 ms2,800
us-west35276 ms7,900
ap-south416104 ms4,600
ap-east488116 ms6,200
sa-east560142 ms9,000
af-south632158 ms3,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.

src/chart/heatmap.css
Errors by region and hour
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.

src/chart/calendar-heatmap.css

None 0 1 to 3 4 to 7 8 to 15 16 and over

Deploys per day
DayDeploys
Tue 3 Feb2
Wed 4 Feb0
Thu 5 Feb5
Fri 6 Feb9
Sat 7 Feb0

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.

src/chart/candlestick.css
Daily open, high, low and close
SessionOpenHighLowClose
1 Apr141.20148.60132.10146.90
2 Apr149.80151.40136.20140.60
3 Apr142.10153.90137.50151.20
29 Apr163.40172.40162.10168.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.

2. Stagger. Twice the labels at full legibility, for one extra line of axis height.

3. Rotate. Last resort, for labels that are genuinely long. It reserves the vertical room explicitly.

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.