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.