Telemetry graph
The widget through which a student watches one or more measured quantities change over time — speed, force, acceleration, temperature — as curves on a shared clock. It is the larger sibling of the single-channel readouts (scale, thermometer, ammeter), which show one number now. The telemetry graph shows several quantities across the whole run, so a student can read trend, lag, and the relationship between channels at a glance.
Live · smart-cart telemetry in context
The widget the way a student meets it — inside the kit's Simulating screen, framed by the permanent on-screen chrome that surrounds every step: the step bar (top), the playback row (bottom), Restart (bottom-left), and the camera controls (bottom-right). That chrome is decorative here; the <lab-widget-telemetry-graph> in the middle is the live production element, driven by synthetic smart-cart dynamics — speed (m/s, a ramp with a gentle oscillation), force (N, periodic impulses), and acceleration (m/s², derived from the speed curve). Poke it: hide a channel from the legend, or hover for the crosshair readout. The driver buttons below reach the widget through its public API.
pushFrame() ~20×/s · buttons hit the public API.
Live · second skin · compact side dock
The same widget, one element, switched by the skin attribute ("full" default, or "side") — the two skins are CSS-differentiated, not separate elements. The "side" skin is a compact, landscape panel for a right-edge dock — vertically centred in the band between the step bar and the playback row — so the curves stay in view while the centre of the screen is free for the experiment. Same self-contained chrome and the same live element (driven by the same synthetic cart), just tightened: a vertically stacked legend, and it fills its container height up to a max-height.
A · What it is
A single-channel readout answers “what is the value now?” The telemetry graph answers “what has the value been doing, and how does it relate to the others?” It plots each channel as a curve against one shared time axis, auto-scales the Y axis to the visible data, and shows the whole run on a time base that follows the simulation.
This is a presentation surface only. The widget receives samples — pushSample(channelId, value, t) for one reading, or pushFrame({ t, values }) for a synchronized set — and draws them. It does not know where the numbers come from. In the lab, the Unity simulation will emit telemetry frames; the curriculum author will declare which channels a step exposes. Both of those are downstream of this element and out of scope here (see the closing note).
It is the sibling of the readouts in the design system: scale, thermometer, ammeter, force sensor. Where those commit a single instantaneous value, the graph is the right tool whenever the shape over time is the lesson — a cart accelerating, a spring oscillating, a beaker cooling.
B · Anatomy
The widget is a hybrid render. The curve traces are drawn on a Canvas 2D layer sized to the device pixel ratio for crisp lines; everything else — the panel frame, axes, gridlines, tick labels, legend, and hover crosshair — is DOM and SVG inside the shadow root, so it stays accessible and pixel-snapped.
- 1Header — a title strip (derived from the channel labels, or the
widget_labeloverride) with a collapse chevron; clicking it collapses the body to the strip. Below it, the plot area — the Canvas trace layer under the DOM axis overlay; one curve per visible channel. - 2Y axis & ticks — always auto-fitted to the visible data, with “nice” rounded tick values; all numerics in the mono face with tabular figures.
- 3X axis — the shared time axis, labelled below, spanning the whole run as the simulation drives it.
- 4Crosshair readout — on hover, a vertical line with each channel's value at that instant.
- 5Legend — one chip per channel, colour-keyed; click to show or hide that curve. The legend is the only interactive control.
C · Channels & legend
Channels are configured with a property, not attributes — el.setChannels([…]), each entry { id, label, unit, color?, hidden? } (no min / max — there is no fixed-scale mode). A channel without an explicit color is auto-assigned the next Lab DS series token (--color-series-1 … --color-series-6, wrapping after six), so a freshly configured graph is colour-coded without the author choosing hues. The series tokens are a dedicated set — they carry no temperature semantics and are not the pour colours.
Each channel gets a legend chip in its curve colour. Clicking a chip hides or shows that curve and fires a channel-toggle event (detail { id, hidden }); the Y axis re-fits to whatever remains visible, so hiding a large-magnitude channel lets the smaller ones breathe. A channel can also start hidden (hidden: true). Show / hide is a view state — the samples keep accumulating in the buffer either way, so un-hiding brings the full trace back.
D · Scale & axes
The Y axis always auto-fits. It tracks the visible data — pads the data range slightly so the curves don't touch the frame, and picks rounded tick values. There is no fixed-scale mode and no Auto / Fixed toggle: the axis simply follows whatever is on screen. With multiple channels of different units sharing one axis, the fit spans the combined range — v1 deliberately keeps a single shared Y axis; per-channel axis assignment is deferred.
The time base follows the run. The X axis shows all data over the run; the feed is external, so the time base follows the simulation as it drives samples in. There is no window selector. window-seconds survives only as an optional, no-UI config attribute (default 0 = show all); set it to clamp the visible history if a step ever needs to, but nothing in the chrome exposes it.
E · Interaction model
The graph is a pure display. There are only two interactions:
- Legend toggle — click a chip to show or hide its curve; this fires
channel-toggle(detail { id, hidden }), the widget's only event (see Channels & legend). - Hover crosshair — moving the pointer over the plot drops a vertical line and reads out each visible channel's value at that instant, in the mono face.
There is no Pause, no autoscale toggle, and no window selector. The feed is external — the simulation drives it — so there is nothing in the widget to stop it; to halt the trace you stop the sim. Deferred for v1, deliberately: per-channel Y-axis assignment and manual axis-range editors. The widget honours prefers-reduced-motion — data still updates, but non-essential easing is skipped.
F · States
The gallery below drives five instances through their distinguishing states. Each is a live element, configured and fed by the page script — the same code path the lab will use.
G · How it works
Hybrid render. Curves go on a Canvas 2D layer because many points per frame on a streaming plot are cheap to draw and expensive as DOM. Everything that benefits from being real DOM — the panel chrome, axes, gridlines, tick labels, legend chips, and the crosshair readout — lives in the shadow root as low-specificity, BEM-ish classes. The canvas is sized to devicePixelRatio for crisp lines, and a ResizeObserver re-fits it when the container changes; the observer is disconnected on teardown.
Pure plot math. The value → pixel and time → pixel mappings, the “nice” tick generator, the data-range padding, tick formatting, and window pruning all live in a DOM-free module (plot-math.js) that the element imports. Keeping the geometry separate from the rendering makes it unit-testable and keeps the element focused on draw + wiring.
Lab DS adherence. The panel is --color-surface on a 2px --color-text border with a hard offset shadow and --radius-md, and every numeric — tick labels, legend values, crosshair readout — in the mono face with tabular figures. Curve colours come from the new --color-series-1 … --color-series-6 tokens via getComputedStyle; secondary text uses --color-text-tertiary. Light-only, single-mode — consistent with the rest of the lab surface.