SPLap

SPLap 2.1.1 · engineering notes

Visualization architecture (Pit Wall 2.1)

This note explains how the Pit Wall draws a real session at 50–60 fps inside a classic Splunk Simple XML dashboard without firing a search per animation frame, and how the pieces can be reused outside SPLap.

1. Three planes, one contract

Plane What it holds Source Cost model
Replay Track geometry, 500 ms replay frames, palettes Static assets (appserver/static/sessions/<id>/…) for the packaged session; splap:replay_frame + splap:circuit events for an imported session Loaded once per session. Animation reads memory only.
Analysis Laps, stints, pits, positions, intervals, race control, weather, overtakes, raw telemetry | inputlookup splap_monza_2025_* or index=splap sourcetype=splap:* Two searches at load (lap/stint/pit; race_control/overtake/weather/interval) + one lazy telemetry search per selected driver, zero per frame
Live Not available in 2.1 Feature-detected; the UI never pretends

js/session_backend.js hides the difference: LookupBackend(entry) and IndexBackend(index, session_key) both expose meta(), frames(), events(types) and splEvents()/splTelemetry()/splFrames() (the SPL strings shown in Show SPL). The Pit Wall never branches on the backend kind except to phrase the SPL contract.

Every search goes through S.search() (js/splap_core.js), which records the SPL, explanation, dataset, runtime, scan count and result count into a contract object. The Splunk panel on the Pit Wall renders those contracts, so "Show SPL" is always the query that actually ran. S.search() strips a leading search because SplunkJS's SearchManager prepends its own; results are paged from the job's results endpoint in 20 000-row pages (status_buckets: 0, max_count: 500000) so a 60 000-row telemetry pull arrives whole.

2. Replay frames and interpolation

Replay frames are produced once, at import (tools/splapdata/frames.py or importer.js I.buildFrames): one row per driver per 500 ms (250–500 ms allowed) with x, y, track distance, heading, speed, gear, throttle, brake, DRS, position, gap, interval, lap, compound, tyre age, pit flag/progress and a coverage bit. The compact form is column-oriented per driver ({cars: {"4": {x: [...], y: [...], ...}}}) so 1 620 frames × 20 cars is ~1.6 MB of JSON and parses in tens of milliseconds.

CircuitRenderer.interpolate(frames, t) returns the car states at an arbitrary time by linear interpolation of x/y and track distance between the two bracketing frames (heading via shortest-arc angle interpolation, discrete fields from the earlier frame). If either bracketing frame has coverage = 0 the car is reported with x = null and the renderer hides it: missing telemetry is never drawn as a car at (0, 0) or a speed of 0.

The animation loop (pit_wall.js tick/loop) runs on requestAnimationFrame, advances st.t by wall-clock × speed (0.25×–4×), interpolates, calls renderer.setCars(), and updates the timing tower and driver card at a lower cadence (the tower re-renders only when the ordered snapshot changes). SPLAP.state.fps and dropped are exposed for the test suite. Reduced-motion users get a paused replay with the scrubber and keyboard stepping.

3. CircuitRenderer (hybrid SVG + Canvas)

js/circuit_renderer.js has no Splunk dependency:

var r = new CircuitRenderer(containerEl, trackGeometry, { orientation: 'fit', cornerLabels: true, reduceMotion: false });
r.setCars(states); r.setLayer('speed', { values, min, max }); r.setRaceControl(zones);
r.on('select', fn).on('hover', fn).on('hovermove', fn); r.resize(); r.destroy();

Static layer (SVG, built once per resize) — asphalt ribbon offset from the centreline using the derived track width (Math.max(11 px, width_m × pxPerM × 1.15)), curbs on the outside of corners, pit lane as its own road with entry and exit markers, start/finish line and label, three sector boundaries, DRS zones (detection line + activation stretch), numbered corner labels placed on the outside of each corner with collision nudging, and the venue plate ("MONZA · ITALY", length, corner count). Orientation fit rotates the geometry by −heading[0] so the main straight is horizontal and the map fills the panel; north keeps north-up.

Dynamic layer (Canvas, every frame) — top-down car glyph (_carPath: nose, sidepods, rear wing) in the team primary with a secondary/accent stripe and a contrast outline, length Math.max(26, Math.min(32, W / 34)) px so cars are readable on a 1440-wide panel and never overpower a 1920 one. Each car carries a label chip with the driver acronym and number; _drawLabels places chips in priority order (selected, leader, battling, then by position) trying ten anchor positions around the glyph and skipping any that overlap a placed chip or another car, with a leader line when the chip lands far from its car, so labels stay legible when the field bunches at the first chicane. Selected car: ring + brighter label; leader: gold ring; DRS open: green wing; in pit lane: dimmed and moved onto the pit path by pit_progress; a fading trail of the last 24 positions when motion is enabled.

Layers recolour the centreline segments from per-frame values (speed, throttle, braking, gear, delta to selected, tyre) or draw race-control zones (yellow sectors, SC/VSC/red as full-track bands). Layers are computed from the frames already in memory; switching a layer costs no search.

Interaction — a single pointer handler hit-tests glyphs and label chips (_hit) and emits hover/select. The Pit Wall uses hover for the floating card (acronym, position, gap, speed, tyre) and select to load the driver card and the lazy telemetry search.

4. Timing tower and driver card

The tower (renderTower) is a 7-column grid per row: position, team colour bar, acronym, number, gap (toggle gap to leader / interval; lapped cars show +n LAP), tyre badge (compound letter in the compound colour plus age), and a status cell (PIT, position-change arrow with count over the last lap, purple dot for the current fastest lap). Rows are keyboard-focusable; j/k move the selection through the tower.

The driver card shows: name, team, position; gap and interval (explicitly labelled); lap; speed, gear, throttle, brake, DRS (from the frame); tyre compound and age; pit status; last lap and best lap (from lap events); the provenance chips (TELEMETRY · OBSERVED 3.7 Hz, POSITION OBSERVED/DERIVED, PALETTE CURATED). The traces panel draws the last 60 s of speed/throttle/brake/gear/DRS from the raw 3.7 Hz telemetry once it has loaded, falling back to frame values before that.

5. Race Engineer findings

js/engineer_rules.js is deterministic and pure: R.run({rows, session, drivers, backend, frames}) over the analysis rows and R.telemetry(tel, driver, track, project, be, code) over the selected driver's raw telemetry. Every finding carries rule, version, confidence, provenance (observed/derived/modeled), an evidence list, the SPL that reproduces its inputs and a focus (time + driver) that the timeline can jump to. Rules and versions: pit_loss_anomaly 1.0, undercut_overcut 1.0, tyre_degradation_change 1.0, pace_drop_in_traffic 1.0, sector_dominance 1.0, battle_closing_rate 1.1 (rejects interval collapses faster than 3 s/lap as data discontinuities), sc_vsc_strategic_impact 1.0 (modeled), telemetry_anomaly 1.0, braking_point_change 1.0, throttle_application_instability 1.0. Findings are listed newest-first relative to the replay clock and never generated by an LLM; an LLM may only explain or summarise them.

6. Timeline and replay controls

Play/pause (Space), ±10 s (←/→), speed 0.25×–4×, scrubber, and jump buttons for the next pit stop, overtake and flag (f on the keyboard jumps to the next flag). Markers on the timeline come from pit, overtake and race_control events; hovering a marker shows its text. Deep links: ?session=<id>&t=<s>&driver=<n>&layer=<name>&speed=<x> and ?index=<idx>&session_key=<key> for imported sessions; t is only applied when the session matches.

Broadcast mode hides the Splunk panel and findings chrome for a full-bleed map + tower; Engineering mode is the default.

7. Performance and query budget

Measured by tests/browser_suite.py on the packaged session at 1440×900 and 1920×1080:

8. Reuse outside Simple XML

CircuitRenderer, palettes.js and engineer_rules.js are plain AMD modules with no Splunk imports; session_backend.js depends only on S.search. A Dashboard Studio custom visualization or a standalone page can load the same modules with any AMD loader, feed it track.json + replay_frames.json from a bundle, and get the same map.