Coastal Traffic Authority

Strait of Kestrel, 24-month movement observatory, January 2025 to December 2026, 60,000 recorded movements.

loading…

Class

Self-test

computing…

Twelve aggregate checks compare the incremental aggregation structures against a hidden brute-force reference block, plus four scale-maths assertions. Visible by default; it is the first thing an operator should see.

Movements by class, monthly

Data table

Speed distribution

Data table

Speed over ground vs draught

Data table (first 200 of the selection)

Destination ports

Data table

When vessels move

Five weekday bands by four six-hour blocks; each cell is coloured by volume and carries a 24-month sparkline. Click or press Enter on a column to pin its hour block.

Data table

Brush performance

Frame times recorded while you interact. Amber bars exceed the 16.7 ms frame budget.

Method

Scale maths

All scales are plain linear maps x → (x - d0) · (r1 - r0)/(d1 - d0) + r0 with the analytic inverse. Axis ticks use Heckbert's nice-number algorithm: the tick step is rounded to 1, 2, 5 or 10 times a power of ten, then the domain is expanded to whole steps, so every drawn tick lies inside the nice domain and the nice domain contains the data. The self-test round-trips the area scale at 26 points and checks tick containment every run.

Binning rule

The speed histogram uses the Freedman-Diaconis rule h = 2 · IQR · n^(-1/3). Quartiles come from a 0.05 kn fine-grid CDF built inside the single aggregation pass, then the fine grid is remapped to k = clamp(ceil((max-min)/h), 4, 80) equal bins. The rule and the resulting bin count are stated in the panel caption on every render.

Spatial index

Scatter tooltips snap to the nearest record through a uniform grid over the full 60,000-row dataset: 60 × 40 cells in data space, each cell a linked list stored in a typed array (head: Int32Array(2400), next: Int32Array(60000)). A query walks one or two rings of cells around the cursor, never the 60k rows. Hexbin cells carry up to 500 member indices so hover can snap to an individual record even in density mode.

Re-aggregation strategy

Changing any filter re-loops the dataset exactly once. That single pass evaluates four lightweight masks (one per panel family, applying the cross-filter rule that a panel is not shrunk by its own dimension) and fills every aggregation structure plus the filtered index buffer: class-month counts, destination counts, 731 day counters, the 20-cell × 24-month heat table, and the 600-bin speed grid. The unfiltered context layer is computed once at load. Typical pass cost on this machine: well under 2 ms for 60,000 records, so a brush frame is dominated by drawing, not counting.

Self-test results

running…

What I verified against the web before shipping: Heckbert's nice-number thresholds (1.5/3/7 when rounding, 1/2/5 when not) from the original Graphics Gems listing; the Freedman-Diaconis formula h = 2·IQR·n^(-1/3) from its Wikipedia article; and the "monotonically increasing lightness" criterion for sequential colormaps from the Matplotlib colormap guide, which is exactly what the CIELAB L* ramp exploits. ARIA treatment (text summary plus a table alternative plus labelled, keyboard-reachable marks) follows the WAI data-visualisation guidance. The generator is mulberry32 seeded (0x51CE457A), contains no Math.random, and is deterministic: the same 60,000 records every load, and the 12 reference aggregates are recomputed on every self-test run by an independent brute-force pass, then compared against the incremental structures that drive the charts. Tolerances: counts exact; means relative 1e-9; grid-quantile aggregates within one fine-grid step (0.5 min queue, 0.025 m draught, 0.05 kn speed), because the incremental side interpolates on the fine grid while the reference sorts exact values. The only approximation in the whole pipeline is that quantile step.