Skip to content

src/chart - 18 files, plus src/behaviour/chart-interact.js

Charts and analytics

Every chart here is CSS and markup. No canvas, no SVG runtime, no charting library, and no build step. Hover a mark, tab into it, arrow along it, switch a series off from the legend, drag a range. Then turn the script off: every number is still on the page, because the reading was never in the drawing.

The markup panels below are read out of the live examples by site.js. With scripting off the examples still render and the panels stay empty.

Time series

Three series, a tooltip anchored to the point, a crosshair, a readout that survives the pointer leaving, arrow-key traversal inside each series, a legend that switches series off, and a brush over the range. Every one of those is optional behaviour on top of markup that already states all twenty-four readings as text.

chart/line + frame + tooltip + crosshair + brush
Requests per second 21:00: 920 last 24 hours, 3 hour samples
  1. 00:00 340
  2. 03:00 420
  3. 06:00 380
  4. 09:00 550
  5. 12:00 610
  6. 15:00 720
  7. 18:00 830
  8. 21:00 920
  1. 00:00 220
  2. 03:00 270
  3. 06:00 250
  4. 09:00 360
  5. 12:00 410
  6. 15:00 490
  7. 18:00 570
  8. 21:00 630
06:00 to 18:00
Requests per second, one sample every three hours
HourRequestsCache hitsErrors
00:003402209
03:004202707
06:0038025012
09:005503608
12:0061041011
15:0072049014
18:0083057010
21:009206306
Show codeHide code

Tab once to reach the first series, then use the arrow keys to walk it. Home and End jump to the ends. Enter or Space pins a point. Escape clears the readout. Tab again for the second series, then for the legend buttons, then for the two range handles. The whole chart is four tab stops, not twenty-four, which is what a roving tabindex is for.

Interactive marks

The same states on a bar run: hover, focus, selected, and dimmed because another series is being pointed at. Rows on a phone and columns from 30em, because seven upright columns with seven labels under them do not fit in 288 pixels and rows always do. Resize the window and watch it turn.

chart/mark.css, states for points, bars and slices
Deploys by day Week total 96
  1. Mon 14
  2. Tue 19
  3. Wed 25
  4. Thu 17
  5. Fri 12
  6. Sat 6
  7. Sun 3
Show codeHide code

Bar and column

Rows by default, columns under a modifier, and the value label always outside the track: a number printed inside its own bar is unreadable the moment the bar is shorter than the text, and no CSS can conditionally move it out.

chart/bar.css
Requests by region 0 to 40,000 per hour
eu-west
32,800
us-east
25,600
ap-south
16,400
sa-east
4,800
Show codeHide code
chart/bar.css, --column and grouped
Signups by month 0 to 1,200
Jan
408
Feb
564
Mar
732
Apr
696
May
948
Jun
1,116
Plan mix by quarter Texture, not hue, tells the two series apart
Q1
Pro
620
Team
380
Q2
Pro
710
Team
520

Stacked

One bar split into parts, and the same parts across categories. The part sizes are raw numbers used as flex-grow values, so the consumer never converts anything to a share and one number changing re-proportions the rest.

chart/stacked-bar.css
Storage by class 4.2 TB
  1. Hot 2.14 TB
  2. Warm 1.18 TB
  3. Archive 0.88 TB
Show codeHide code

Pie and donut

Both are one conic gradient with the running totals computed in calc, so the inline values stay the numbers the consumer already has. The legend is the chart; the disc is a second rendering of it and carries aria-hidden for that reason.

chart/pie.css
  1. Direct 42%8,410
  2. Search 27%5,406
  3. Referral 19%3,804
  4. Social 12%2,404
Sessions by source, last 30 days. 20,024 total.
Show codeHide code
chart/donut.css
20,024 sessions
  1. Returning 48%
  2. New 31%
  3. Unknown 21%
Show codeHide code

Funnel

Stages and what survives each one. The taper is a clip-path between this stage's width and the next one's, because CSS cannot read a sibling's custom property and anything that hid that fact would be a script.

chart/funnel.css
Checkout, last 7 days
  1. Cart viewed 20,024 62% continued
  2. Details entered 12,415 66% continued
  3. Payment started 8,210 83% continued
  4. Paid 6,808
Show codeHide code

Treemap

Part to whole where the parts are too many for a pie. There is no squarify pass here and the file says so first: the spans are chosen once by the consumer, which is the right trade for a budget or a service bill and the wrong one for data that changes shape hourly.

chart/treemap.css
Spend by service, October
  1. Compute 41,200
  2. Storage 28,900
  3. Egress 17,400
  4. Database 13,100
  5. 17 smaller services 6,940
Show codeHide code

Scatter and bubble

Two coordinates instead of one, and a third carried by size. The points are the same interactive mark as the time series above, so hover, focus, arrow keys, selection and the tooltip all work without a second implementation. This is the chart where the crosshair genuinely wants both lines.

chart/mark.css --point and --bubble
Latency against load 9 instances bubble area is request volume
  1. api-01 118ms
  2. api-02 132ms
  3. api-03 149ms
  4. api-04 205ms
  5. api-05 228ms
  6. api-06 262ms
  7. api-07 341ms
  8. api-08 368ms
  9. api-09 352ms
Show codeHide code

Single values

A completion ring, a gauge, a meter with bands, and a sparkline. The first is in this group; the other three already live in the application layer, so this page composes with them rather than shipping a second copy of each.

68%
Index rebuild
72% Cache hit rate
Primary volume 412 GB of 512 GB

Elevated. The high mark is 435 GB.

Requests per minute 1,284 low 840, high 1,410, last 12 hours

Live append

A sample every 800ms, appended without measuring anything: the new mark's height is a custom property, the node is cloned from the mark already in the list, appends arriving inside one frame are coalesced, and the window cap drops the oldest in the same write. An unbounded live list is a memory leak with a chart drawn on it, so the cap is not optional.

behaviour/chart-interact.js, appendPoint
Events per second waiting for the feed rolling window of 24 samples
  1. 0

The button is a real toggle with aria-pressed, and the feed is a setInterval in the page rather than in the framework: bare.style has no opinion about where data comes from, only about what it costs to draw.

Analytics dashboard

The tile grid, the period selector, and the comparison against the previous period. One column on a phone, two from 48em, four from 80em, and a tile spans two columns only where two exist: under auto-fit a span of two in a grid that resolved to one column creates an implicit second column and puts the page into horizontal scroll.

chart/dashboard.css

Platform, last 7 days

Sessions

48,201

12.4% up from 42,900

Sessions 48,201

Error rate

0.42%

0.19pp down from 0.61%

Error rate 0.42%

p95 latency

218ms

31ms up from 187ms

p95 latency 218ms

Recurring revenue

84,120

flat from 84,060

84%
of quarter target

Traffic

dotted line is the previous 7 days
Sun: 8,940 sessions per day
  1. Mon 5,200
  2. Tue 5,700
  3. Wed 5,500
  4. Thu 6,700
  5. Fri 7,100
  6. Sat 7,800
  7. Sun 8,940

By source

48,201 sessions
  1. Direct42%
  2. Search27%
  3. Referral19%
  4. Social12%

By region

eu-west
19,700
us-east
15,400
ap-south
9,800
sa-east
3,301

Checkout funnel

34% end to end, 2pp better than last week
  1. Cart viewed 20,024 62% continued
  2. Details entered 12,415 66% continued
  3. Payment started 8,210 83% continued
  4. Paid 6,808

Top pages

share of sessions
Top pages by sessions, last 7 days
PageSessionsShareChange
/pricing11,20423%up 8%
/docs9,88021%up 14%
/8,41217%flat
/changelog4,1099%down 3%
/status2,8806%up 41%
Updated 40 seconds ago
Show codeHide code