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, playback placeholder) 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
Scatter sliceImplemented; limited Storybook coverage
Yellow playback cursor (realm clock)Ready when anchor is "time" and page/row playback controls drive the clock
playback slice (in-chart controls)Not ready for use , reserved in schema; renders a placeholder. Use page-level PlayBackControlDS instead (v2) or the legacy wrapper’s local playback bar
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

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: multiple toggleable event types
legendItemsarrayNoLegend slice: toggle keys
timesnumber[]NoSpanning lines: vertical line positions in x-domain units

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".
  • Do not add a playback element to layouts until the slice is marked ready in docs.

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" } ] }, { "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, playback placeholder, 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" | "square" | "dot" }[]
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, 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).

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
LimitationsOne logical range series per slice

spanning_lines

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

ChannelsNone (use times[] in layout or resolve from data later)
SyncOverlay only
Configtimes[] in x-domain units; style.color, style.visibilityKey
BehaviourToggled via legend; drawn above synced area, below hover capture
LimitationsNo channel-driven resolution in DS yet for all deployments; prefer explicit times in layout for static demos

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

playback (not ready)

Reserved slice type for future in-chart play/pause + scrubber wired to the realm TimelineClock.

StatusNot ready for use , do not add to production layouts
ChannelsNone
SyncNo
Current behaviourRenders placeholder text: “Not ready for use”
AlternativeUse session enableGlobalPlaybackControls or row enablePlaybackControls with PlayBackControlDS; yellow cursor still tracks the clock

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 }[]
staticvalue{ value: <json> }

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 } ] }

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
  • 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.