Skip to Content
This documentation is provided with the HEAT environment and is relevant for this HEAT instance only.
Dashboard ComponentsNextComposableChart (Next)

ComposableChart (Next)

A layout-driven chart widget that stacks multiple UI slices in one container. Time-synced slices share a primary x-domain and interaction state (zoom, pan, hover scrubber). Non-synced slices (legend, static, transcript, playback controls, flight tracks, video playback) sit in the stack but do not participate in x-axis sync.

Use this widget when a design needs mixed chart types (area + event lanes + comms ranges), synchronized scrubbing, per-series legend toggles, and per-slice fault isolation in one panel.

This is the supported production path. It requires:

  • dashboard-v2 runner and $heat-dataservice channel payloads
  • ComposableChartDS in ui/dashboard (not the legacy wrapper)
  • v2 layout row configuration.composableChartItem per heat-layout schema 
  • Page- or row-level PlayBackControlDS driving the realm TimelineClock so the yellow cursor and other playback widgets stay in sync

If you are on Legacy dashboards (ui/legacy, v1 combined-custom-charts), use the bridge documented in ComposableChart (Legacy) instead. Legacy binding has no $heat-dataservice, no cross-widget clock sync, and requires an explicit legacy manifest.

Current state and limitations

AreaStatus
Area (multi-series), legend, events lanes, ranges lanes, spanning linesReady for layout-driven use
Hover scrubber, magnetic snap, tooltipReady
Vertical time grid + bottom tick labelsReady (composableChartItem.xAxis.ticks)
Static slice (StatsCard payload)Ready
Transcript slice (timed utterances)Ready. Optional highlightTerms underlines matching tokens
Scatter sliceImplemented; limited Storybook coverage
Engagement slice (per-target rows, isTargetEngagement mode)Ready for layout-driven use
Yellow playback cursor (realm clock)Ready when anchor is "time" and page/row playback controls drive the clock
playback slice (in-chart scrubber)Ready. Seek bar with playhead timestamp. No play button. Storybook: Organisms/ComposableChart/PlaybackWithAircraftLanes
flight_tracks sliceReady. Dual-pane lat/lon trail + altitude strip, legend-filtered, clock-scrubbed.
video_playback sliceReady. Video-atlas seat tiles following the realm clock; playback bounded to the clock span or videoPlayback.bounds; optional per-seat gaze marker via videoPlayback.gazeChannel. Storybook: Organisms/ComposableChart/VideoPlaybackBounded
markdown sliceReady. Inline Markdown text block. Content from markdownContent in layout (no channel needed). Storybook: Organisms/ComposableChart/MarkdownSliceMock
Index anchorPartial , x-domain derived from longest slice; less tested than time anchor
none anchorStatic-only stacks

General limitations

  • Rendering is SVG + D3 (not ApexCharts). Very large series may need channel downsampling upstream.
  • Slice channel binding uses URI strings on each element, not the top-level configuration.channels mapping.
  • Multiple widgets of the same type in one layout are supported via distinct configuration.name and element channel URIs.
  • Empty label: "" on a slice suppresses the lane title (used for combined area + external legend).

Dashboard generation

Value
Runner nodedashboard-v2
Frontendui/dashboard (ComposableChartDS)
Data envelope$heat-dataservice (contract)
RenderingSVG + D3 scales/zoom

Layout component identifier

ComposableChart , configuration.component in v2 layout rows.

Layout configuration

Typical fields on the row configuration object:

FieldTypeRequiredDescription
componentstringYes"ComposableChart"
namestringYesInternal widget name
titleContentstringYesPanel title
channelsstring[]YesLegacy/base channel list (may be empty when slices bind via URI)
composableChartItemobjectYesSlice definitions and anchor
composableChartItem.anchor"time" | "index" | "none"NoPrimary x-axis semantics (default: "time")
composableChartItem.xAxisobjectNoChart-wide vertical grid and tick labels: { ticks?: number[], timeLabelMode?: "auto" | "elapsed" | "clock" | "dateTime" | "dateTimeLong" }
composableChartItem.elementsarrayYesOrdered slice configs
composableChartItem.isTargetEngagementbooleanNoTarget Engagement mode: renders only legend and engagement elements as stacked per-target rows, disables pan/zoom, and shows the Targets multi-select dropdown
composableChartItem.showMultiDropdownbooleanNoKept alongside isTargetEngagement for parity with the UI types; the dropdown itself is driven by isTargetEngagement

Each element in composableChartItem.elements:

FieldTypeRequiredDescription
typestringYesSlice type (see Slice reference)
ordernumberYesVertical stack order (lower = higher)
labelstringNoLane title; empty string hides the title
channelsstring[]NoChannel URIs: realm:channelId:name or channelId:name
heightnumberNoSlice height in pixels (default 72)
styleobjectNoColors, opacity, marker size, visibilityKey for legend wiring
yAxisobjectNoArea slice only (see below)
seriesarrayNoArea slice: multiple series in one band
eventSeriesarrayNoEvents lane and engagement slices: multiple toggleable event types
rangeSeriesarrayNoEngagement slice: overlaid range bars (id, label, color, tooltipLabel, barHeight)
legendItemsarrayNoLegend slice: toggle keys
timesnumber[]NoSpanning lines: vertical line positions in x-domain units
videoPlaybackobjectNoVideo playback slice: { bounds?: { fromMs, toMs }, enablePlayPause?: boolean, gazeChannel?: string }. bounds are absolute capture-clock ms; gazeChannel is a value channel URI holding a gaze-points payload for the per-seat gaze marker (see video_playback)

Playback for this widget

  • Set enableGlobalPlaybackControls or row enablePlaybackControls on the session layout to show PlayBackControlDS.
  • The chart draws a yellow vertical cursor at xDomain[0] + clock.elapsedMs when anchor is "time".
  • Add a playback element for the in-chart seek bar (no play button). Page-level PlayBackControlDS remains optional.

Layout excerpt (terrain-style timeline)

{ "component": "ComposableChart", "colspan": 12, "configuration": { "name": "TerrainTimeline", "titleContent": "Terrain Timeline", "channels": [], "defaultRealm": "circuit-03", "enablePlaybackControls": true, "composableChartItem": { "anchor": "time", "xAxis": { "ticks": [0, 300000, 600000, 900000] }, "elements": [ { "type": "legend", "order": 0, "height": 40, "legendItems": [ { "id": "speed", "label": "Speed", "color": "#81842C", "swatch": "line", "isLabel": false } ] }, { "type": "area", "order": 1, "label": "", "height": 160, "yAxis": { "domain": [0, 100], "bandLabels": [ { "label": "LOW", "value": 16.66 }, { "label": "MEDIUM", "value": 50 }, { "label": "HIGH", "value": 83.33 } ], "gridLines": [33.33, 66.66] }, "series": [ { "id": "speed", "label": "Speed", "channel": "circuit-03:timeline-speed:Air Speed", "color": "#81842C", "render": "line_gradient", "unit": " km/h" } ] }, { "type": "ranges_lane", "order": 2, "label": "Comms", "height": 32, "channels": ["circuit-03:timeline-comms:Comms Events"], "style": { "color": "#0B5370", "visibilityKey": "comms" } }, { "type": "events_lane", "order": 3, "label": "Commander", "height": 32, "eventSeries": [ { "id": "cmdr_rep", "label": "Reports", "color": "#666666" } ] } ] } } }

Slice reference

Slices render top-to-bottom by order. Synced slices share zoom, pan, hover scrubber, and (when enabled) the yellow playback cursor. Non-synced slices are legend, static, transcript, playback placeholder, flight tracks, video playback, and spanning lines overlay (zero height band).

legend

Toggle row for series visibility. Each legendItems[] entry needs a unique id matching series ids (area series[].id, eventSeries[].id, style.visibilityKey, or spanning line key).

ChannelsNone
SyncNo
ConfiglegendItems: { id, label, color, swatch?: "line" | "vertical-line" | "vertical-dashed-line" | "square" | "dot" | "diamond" | "cross" | "primary" | "secondary" | "bolt", isLabel?: boolean }[]
BehaviourClick toggles visibility for that id across the chart
LimitationsToggle state resets when slice config changes

area

Primary time-series band. Supports multiple series in one slice via series[].

ChannelsOne URI per series[].channel, or legacy single channels[0]
Channel shapeseries → { timeMs, value }[]
SyncYes
Configseries[]: id, label, channel, color, fill, render (line, line_gradient, step_area), unit, tooltipLabel; yAxis: domain, bandLabels, gridLines
BehaviourLinear or step interpolation; gradient fill for speed-style lines; hover nubs on scrubber; y-axis band labels (e.g. LOW/MEDIUM/HIGH) are config-driven, not hardcoded
LimitationsSingle-channel legacy path only supports one series; prefer series[] for multi-series

scatter

Points plotted by time (x) and value (y).

Channelschannels[0]
Channel shapeseries
SyncYes
Configstyle.color, style.markerSize
LimitationsOne series per slice; y-scale auto from data extent

events_lane

Horizontal lane of event dots at fixed y. Supports multiple event types per lane via eventSeries[] (e.g. Commander Reports + Commands).

ChannelsOptional per eventSeries[].channel; static times[] for mocks/tests
Channel shapeevents → { timeMs, occurred }[], or boolean samples on a series channel
SyncYes
ConfigeventSeries[]: id, label, color, tooltipLabel, shape (same vocabulary as legend swatches, including bolt), optional channel / times
BehaviourEach series toggled independently via legend; magnetic snap on hover; dots grow when highlighted
LimitationsLane collapses visually when all its series are hidden; single-channel legacy mode uses one color

ranges_lane

Horizontal bars for intervals (e.g. comms active periods). Supports single-series and multi-series modes.

Single-series — one ranges channel, one colour:

Channelschannels[0]
Channel shaperanges → { startTimeMs, endTimeMs, durationMs }[]
SyncYes
Configstyle.color, style.visibilityKey (legend id)
BehaviourHighlight when hover/scrub time falls inside range; tooltip shows duration

Multi-series — multiple named, individually coloured series sharing the same lane row:

ChannelsNone at top level — each series supplies its own channel
Channel shaperanges per series
SyncYes
ConfigrangeSeries[]: id, label, color, tooltipLabel, barHeight, channel. Each series is toggled independently via legend
BehaviourAll series render at the same vertical position on the lane; series hidden via legend collapse the lane when all are hidden

engagement

One per-target row combining overlaid range bars and an event marker stream, used with composableChartItem.isTargetEngagement: true to build a stacked multi-target Target Engagement panel (one engagement element per target).

Channelschannels[0], exactly one channel per element
Channel shapevalue with data.value.kind: "engagement" (see Example channels)
SyncYes (pan/zoom disabled in isTargetEngagement mode)
ConfigrangeSeries[]: id, label, color, tooltipLabel, barHeight; eventSeries[]: id, label, color, tooltipLabel, shape (dot/diamond/cross/primary/secondary/bolt)
BehaviourRow title comes from the channel’s engagementData.title, which also populates the Targets multi-select dropdown; elements whose channel has no data are dropped, so layouts can over-provision elements (fixed channel ids engagement-target-1..N) and adapt to the session’s real target count
LimitationsRange bars render solid fills only (no dashed or pattern fills); rangeSeries/eventSeries entries match engagementData arrays by id, unmatched ids are ignored silently

spanning_lines

Vertical lines spanning the synced stack (e.g. hits received). Not a visible slice band (height: 0).

ChannelsOptional. One events channel, resolved against the selected realm. Required on any layout with a realm filter, see below
SyncOverlay only
Configchannels[] (preferred) or static times[] in x-domain units; style.color, style.visibilityKey
BehaviourToggled via legend; drawn above synced area, below hover capture
LimitationsA bound channel that resolves to no events drops the slice rather than falling back to times[]

Static times cannot follow a realm filter

times[] is resolved when the layout is composed, on the runner, before any realm exists. On a dashboard with a realm filter the selection is made in the browser and passed to widgets at render time, so a static array shows the same lines under every realm. A hit-marker lane built this way will show every aircraft’s hits no matter which aircraft is selected.

Bind a channel instead and the markers follow the dropdown, the same way area series already do:

{ "type": "spanning_lines", "order": 2, "height": 0, "label": "Hits Taken", "channels": ["timeline-hits"], "style": { "color": "#C13D40", "visibilityKey": "hits_taken" } }

The channel must carry shape: "events"; entries with occurred: true inside the current x-domain become lines. Other shapes fall back to the same truthy-value rule events_lane uses. Static times[] remains supported for layouts with no realm filter, and takes effect only when no channel is bound.

markdown

Inline Markdown text block rendered inside the chart stack. Content is embedded directly in the layout element via markdownContent — no channel or runner payload is required.

ChannelsNone (content is layout-embedded)
SyncNo
ConfigmarkdownContent (Markdown string on the element); height (default 120)
BehaviourRenders Markdown via MarkDownContent atom in a foreignObject; 12 px inset top and bottom
StorybookOrganisms/ComposableChart/MarkdownSliceMock

static

Non-chart content block (typically StatsCard weather summary).

Channelschannels[0]
Channel shapevalue → { value: { statsCard: [...] } } or similar JSON
SyncNo
Configstyle.displayInSingleRow
BehaviourRenders HTML via foreignObject; errors isolated to slice
LimitationsOnly StatsCard-shaped payloads are styled; other JSON shows fallback text

transcript

Out-of-band scrollable transcript list synced to the shared realm clock. Use under a ranges_lane (for example Comms occupancy) when you need utterance text.

Channelschannels[0]
Channel shapevalue → [{ start, end, text }, ...] or { segments: [...] } (start/end elapsed ms)
SyncNo (not in scrubber / playback-cursor bands)
Configlabel (header, default Transcript), style.color (active row accent), highlightTerms (optional string list to underline), height (default 160)
BehaviourHighlights the row whose [start, end] contains playback.elapsedMs; click seeks to segment start; auto-scrolls active row into view
LimitationsNo collapse control in v1; requires page-level playback for highlight

playback

In-chart seek bar wired to the realm TimelineClock. No play/pause button and no duration readout. Page-level PlayBackControlDS remains available when a design needs those controls.

StatusReady
ChannelsNone
SyncSeeks playback.onSeek
BehaviourSlider with a persistent playhead timestamp (layout: scrubber)
StorybookOrganisms/ComposableChart/Slices/Playback, Organisms/ComposableChart/PlaybackWithAircraftLanes

flight_tracks

Dual-pane spatial slice: left is a lat/lon trail canvas, right is an altitude strip. Both panes share the same named tracks, the chart legend, and the realm clock.

Channelschannels[0]
Channel shapevalue → { tracks: [{ id, label, color, points: [{ x or timeMs, lat, lon, alt }] }] }
SyncNo (not in the x-axis overlay band). Scrubs from playback.elapsedMs.
ConfigflightTracks.leftPercent (default 72), flightTracks.topDownView (default true), flightTracks.trailMs (default 180000), flightTracks.terrain (optional offline basemap, see below), height (default 288)
BehaviourLegend ids hide a track from both panes. Trails truncate at the playhead.
StorybookOrganisms/ComposableChart/Slices/FlightTracks, Organisms/ComposableChart/Slices/FlightTracksWithTerrain, Organisms/ComposableChart/PlaybackWithAircraftLanes

Offline terrain (flightTracks.terrain)

Aircraft in real-world space can be drawn over a ground image, a low-resolution heightmap, and mission-planning overlays. The dashboard bundles none of it: the slice points at a manifest JSON that an operator publishes through Cluster Manager env-assets (a .heatpatch with target env-assets, or the hxefs API), so terrain varies per project and customer.

{ "type": "flight_tracks", "flightTracks": { "terrain": { "manifestUrl": "/env-assets/terrain/ol-gulf/v1/terrain.json" } } }

The manifest (relative URLs resolve against it) declares:

  • bounds: { minLat, maxLat, minLon, maxLon } in degrees, EPSG:4326 (plate carree). Keep rasters in plain lat/lon, not Web Mercator, so the pixel grid maps linearly onto the viewer.
  • ground: { url, opacity }, a north-up image draped over the bounds. Opacity defaults to 0.5 so bright imagery sits behind the trails; raise it per manifest or per slice with terrain.opacity.
  • height: { url, minM, maxM, verticalOffsetM }, a greyscale heightmap. verticalOffsetM reconciles an orthometric DEM with the ellipsoidal track altitudes.
  • groundLevelM: flat ground altitude when no heightmap is used (default 0).
  • overlays: { type: "image", url, bounds, opacity } or { type: "polygons", url | geojson, color, lineWidth, labels }. The GeoJSON may hold Polygon / MultiPolygon rings (closed outlines), LineString / MultiLineString paths (open), and Point / MultiPoint markers. Simplestyle properties on each feature (stroke, stroke-width, stroke-opacity, marker-color, Name) override the overlay defaults, so a geojson.io style mission-planning export keeps its authored colours and names. labels: false hides feature names. Fills are parsed but not drawn yet. A third overlay type, { type: "scenario-auto", url: "index.json" }, points at a catalogue { scenarios: [{ slug, title, overlayUrl }] } of per-scenario GeoJSON. By default (mode: "common") the widget draws only the features present in every scenario, such as corridors and boundaries, and never a scenario-specific one, so nothing that could be wrong is shown and no per-session configuration is needed. mode: "auto" opts into detection: each scenario is scored by how much of the flown track lies inside its polygons or within 8 km of its lines (shared features carry no weight) and its specific features draw only when it leads the runner-up by minLead (default 1.25) and clears minScore (default 0.05). mode: "off" draws nothing from the catalogue. An operator switches modes by editing terrain.json on the cluster; the slice’s terrain.scenarioSelection can override it from the template.

Slice-side options: manifest (inline instead of manifestUrl), showHeight, showOverlays, opacity, segments (drape subdivisions, default 96), and verticalExaggeration (terrain relief only, default 1). To lift the aircraft and the terrain together so climb, descent and relief read in the perspective view, set flightTracks.verticalScale (default 1, true scale; 6 works well over flat country). The two multiply. Terrain is placed through the same centre and scale transform as the tracks, so it stays aligned with them, and the whole terrain group renders beneath the flight paths so trails and markers are never hidden by the ground, an image overlay, or mission vectors.

video_playback

Video-atlas seat tiles inside the chart stack. Renders the same N-up seat view as the standalone VideoPlayback column, but follows the chart’s realm clock and plays only the window the chart is looking at.

StatusReady
Channelschannels[0]: one value channel holding a video-atlas manifest, written as <realm:>video_atlas:video_atlas (channel id and name identical). Optionally a second value channel, named by videoPlayback.gazeChannel (<realm:>id:name, or a bare id when id and name are identical), holding a gaze-points payload. List both channels on the chart column channels.
Channel shapevalue → video-atlas manifest (see the VideoPlayback data contract); gazeChannel value → gaze-points payload (see Gaze overlay)
SyncNo (non-synced HTML panel, like flight_tracks). Follows the realm clock for seek position and play state.
ConfigvideoPlayback.bounds ({ fromMs, toMs }, absolute capture-clock ms, same domain as the manifest epochMs, fromMs < toMs); videoPlayback.gazeChannel (value channel URI of the gaze-points payload; omit for video without the overlay; the Arbex builder throws unless it is a non-empty string); videoPlayback.enablePlayPause (accepted, currently ignored); height (default 240)
BehaviourOne tile per seat in the atlas geometry, including placeholder regions (painted black with a “no video” caption, never blitted). Playback is bounded: by default the playable window is the realm clock’s [originMs, originMs + durationMs]. A vignette realm’s clock spans only the vignette, so inside a vignette modal the slice plays, seeks, fetches and buffers only the atlas windows inside the vignette. videoPlayback.bounds overrides that window. With no origin on the clock and no bounds the whole atlas plays (the standalone-dimension case). Windows outside the bounds are never fetched. With gazeChannel set, each tile draws a marker at the projected gaze position for the playhead, joined to the region by originUuid. The widget samples at xDomain[0] + clock elapsed, holds a sample for about two bucketMs, then shows no marker. An off-frame look (u or v outside 0..1) pins a dashed marker to the tile edge. A seat with no track shows no marker; placeholder seats still get one.
PairingAdd a playback slice for the scrubber and one events_lane per seat for gaze lanes. Publish the gaze-points channel in every realm the slice is mounted in, clipped to that realm’s span. Play/pause belongs to the playback slice or page-level controls, not to this slice.
StorybookOrganisms/ComposableChart/VideoPlaybackBounded

Layout excerpt (eye tracking, one vignette realm; the default window is the vignette clock span, so no bounds are set; gaze_points is the gaze-points value channel, id and name identical):

{ "component": "ComposableChart", "colspan": 12, "configuration": { "name": "EyeTracking", "titleContent": "Eye Tracking", "channels": ["video_atlas", "gaze_points", "gaze-seat-1", "gaze-seat-2", "gaze-seat-3", "gaze-seat-4"], "defaultRealm": "vignette-01", "composableChartItem": { "anchor": "time", "elements": [ { "type": "legend", "order": 0, "height": 40, "legendItems": [ { "id": "fixation", "label": "Fixation", "color": "#7EB8DA", "swatch": "dot" } ] }, { "type": "video_playback", "order": 1, "height": 240, "channels": ["vignette-01:video_atlas:video_atlas", "vignette-01:gaze_points:gaze_points"], "videoPlayback": { "gazeChannel": "vignette-01:gaze_points:gaze_points" } }, { "type": "playback", "order": 2, "height": 40 }, { "type": "events_lane", "order": 3, "height": 36, "label": "Seat 1", "eventSeries": [ { "id": "fixation", "label": "Fixation", "channel": "vignette-01:gaze-seat-1:Fixation", "color": "#7EB8DA", "shape": "dot" } ] }, { "type": "events_lane", "order": 4, "height": 36, "label": "Seat 2", "eventSeries": [ { "id": "fixation", "label": "Fixation", "channel": "vignette-01:gaze-seat-2:Fixation", "color": "#7EB8DA", "shape": "dot" } ] }, { "type": "events_lane", "order": 5, "height": 36, "label": "Seat 3", "eventSeries": [ { "id": "fixation", "label": "Fixation", "channel": "vignette-01:gaze-seat-3:Fixation", "color": "#7EB8DA", "shape": "dot" } ] }, { "type": "events_lane", "order": 6, "height": 36, "label": "Seat 4", "eventSeries": [ { "id": "fixation", "label": "Fixation", "channel": "vignette-01:gaze-seat-4:Fixation", "color": "#7EB8DA", "shape": "dot" } ] } ] } } }

Data contract

Publish canonical $heat-dataservice with realms, groups, and channels per the dashboard-v2 upstream contract.

Channel shapes summary

Slice typeChannel shapeData used
areaseries{ timeMs, value }[]
scatterseries{ timeMs, value }[]
events_laneevents{ timeMs, occurred }[]
ranges_laneranges{ startTimeMs, endTimeMs, durationMs }[]
engagementvalue{ value: { kind: "engagement", series: [], engagementData: { title, events, ranges } } }
staticvalue{ value: <json> }
transcriptvalue[{ start, end, text }, ...] or { segments: [...] }
flight_tracksvalue{ tracks: [{ id, label, color, points: [{ timeMs, lat, lon, alt }] }] }
video_playbackvalueVideo-atlas manifest, passed through unchanged (see VideoPlayback); optional gazeChannel value holding a gaze-points payload ({ formatVersion: 1, byOrigin: { [originUuid]: { t, u, v } } }, see Gaze overlay)

Example channels

Series (area / scatter):

{ "id": "timeline-speed", "name": "Air Speed", "groupId": "timeline", "shape": "series", "data": [ { "timeMs": 0, "value": 50 }, { "timeMs": 60000, "value": 70 } ] }

Events lane:

{ "id": "timeline-hits-taken", "name": "Hits Taken", "groupId": "timeline", "shape": "events", "data": [ { "timeMs": 198000, "occurred": true } ] }

Ranges lane:

{ "id": "timeline-comms", "name": "Comms Events", "groupId": "timeline", "shape": "ranges", "data": [ { "startTimeMs": 72000, "endTimeMs": 120000, "durationMs": 48000 } ] }

Engagement (one channel per target row):

{ "id": "engagement-target-1", "name": "engagement-target-1", "groupId": "targets", "shape": "value", "data": { "value": { "kind": "engagement", "series": [], "engagementData": { "title": "Target 1", "events": [ { "id": "hit", "times": [1500, 2500] }, { "id": "neutralised", "times": [12000] } ], "ranges": [ { "id": "exposed", "data": [{ "startX": 1000, "endX": 12000 }] } ] } } } }

events[].id and ranges[].id must match the element’s eventSeries[].id and rangeSeries[].id. See the full example in tools/arbex/reference/next/ComposableChart-target-engagement-example.json.

Interactions

InteractionDescription
Zoom / panMouse wheel / drag on synced area; all synced slices rescale together
Hover scrubberGrey vertical line + tooltip (time, interpolated area values, active ranges, snapped events). Time label follows xAxis.timeLabelMode (default auto: elapsed under 4h, clock through 3d, date+time through 14d, then month+year).
Magnetic snapPointer snaps to nearby event dots (stronger when over dot, weaker on vertical track)
Legend togglesShow/hide series, lanes, spanning lines independently
Playback cursorYellow line at clock position (requires external playback controls, not the playback slice)
Time gridDashed vertical lines at xAxis.ticks with labels using the same timeLabelMode rules as the hover header

Fault isolation

Each slice is wrapped in an error boundary. Invalid config or missing data shows an inline empty state for that slice only.

Anchor modes

AnchorX-axisSync
time (default)timeMs from channel time rangesFull zoom/scrub/playback cursor
index0..N from longest sliceZoom/scrub across index
noneN/AStatic slices only

Storybook

  • TerrainTimelineMock , reference-aligned legend, combined area, comms, commander/gunner/driver lanes, time grid
  • TerrainTimelineWithSummary , above plus StatsCard static slice
  • CommsWithTranscript , Pilot ranges lane plus transcript viewer
  • Per-slice , Organisms/ComposableChart/Slices/* (area, scatter, events, ranges, legend, static)
  • DataService , live mock client when loaded

The playback slice is intentionally omitted from Storybook mocks until it is product-ready.