ComposableChart (Legacy)
Stacked, synchronized multi-slice charts on Legacy dashboards using v1 combined-custom-charts payloads and an explicit legacy manifest (layout + bindings). Rendering uses the same SVG/D3 ComposableChart presentational component exported from heat-next, wrapped by ComposableChartLegacy.
When to use legacy binding vs v2 full support
Full, production-grade ComposableChart support is the v2 path: ComposableChartDS in ui/dashboard with $heat-dataservice, channel URIs, and page-level PlayBackControlDS driving TimelineClock. Use this legacy binding only when you must ship on Legacy dashboards (ui/legacy, v1 facade) and cannot migrate the page to dashboard-v2 yet.
| Legacy binding (this doc) | v2 full support | |
|---|---|---|
| Frontend | ui/legacy + heat-next ComposableChartLegacy | ui/dashboard + ComposableChartDS |
| Data | v1 combined-custom-charts (DateTime strings) | $heat-dataservice channels (series / events / ranges) |
| Layout config | composableChartLegacyManifest on layout col (legacy-only) | composableChartItem in v2 layout schema |
| Playback sync | Local scrubber in widget only | Realm TimelineClock syncs with map and other DS widgets |
| Channel URIs | Not used | Required per slice |
| Manifest validation | TypeScript + docs only | Partial JSON schema in heat-layout-schema.json |
Dashboard generation
| Value | |
|---|---|
| Runner node | dashboard (v1) |
| Frontend | ui/legacy via heat-next (ComposableChartLegacy) |
| Legacy API / facade key | combined-custom-charts |
| Presentational chart | heat-next ComposableChart (SVG + D3) |
Layout mount identifier
ComposableChart , set on each column in layoutConfiguration:
{
"component": "ComposableChart",
"colspan": 12,
"marginBottom": 24,
"configuration": {
"composableChartLegacyManifest": { }
}
}If composableChartLegacyManifest is omitted, the legacy wrapper shows an empty state. Production layouts must embed an explicit manifest (layout + bindings). The terrain prototype manifest exists only for Storybook and unit tests in ui/dashboard.
Data payload key
combined-custom-charts , array of chart objects published by the v1 dashboard node (same source as TimelineChart on legacy pages).
{
"combined-custom-charts": [
{
"title": "Terrain timeline",
"datasets": [
{
"name": "Air Speed",
"type": "area",
"data": [{ "DateTime": "2025-02-17T15:36:37.000Z", "Data": 50 }]
}
]
}
]
}Limitations
| Limitation | Detail |
|---|---|
| No channel URIs | Bindings map v1 dataset names to slices; no $heat-dataservice ingest |
| No cross-widget clock | Local playback only; does not sync with map or other widgets |
playback slice | Not supported , reserved; renders placeholder if present in layout |
| Manual bindings | No auto-generation from combined-custom-charts without a manifest |
| No legacy manifest JSON schema | Documented TypeScript shape only |
| Spanning line times | Must be session-relative ms in layout (element.times), not v1 DateTime |
| x-axis ticks | Session-relative ms in layout.xAxis.ticks, aligned with resolver x-domain |
| Static slice | Inline static binding value only (no separate v1 stats key resolver yet) |
v1 dataset types
The resolver reuses the same type rules as v2 ingest extractTimelineData:
v1 dataset.type | Resolved slice data | Typical slice |
|---|---|---|
area, line (default series) | { kind: "series" } or multi-series in one area slice | area |
scatter1, scatter | Event times | events_lane |
rangeBar | { kind: "ranges" } | ranges_lane |
Series point fields
| Field | Description |
|---|---|
DateTime or StartTime | ISO timestamp (absolute); converted to session-relative x |
Data, value, or Duration | Numeric y value for area/scatter series |
Range point fields
| Field | Description |
|---|---|
StartTime, EndTime | ISO timestamps for range start/end |
Event point fields
| Field | Description |
|---|---|
DateTime or StartTime | ISO timestamp for discrete event |
Legacy manifest shape
Legacy manifests are not part of the v2 heat-layout-schema.json. They pair a v2-compatible layout (slice vocabulary) with bindings that describe how each element.order maps to v1 data.
type ComposableChartLegacyManifest = {
layout: ComposableChartLayoutItem; // same slice types as v2 layout
bindings: LegacySliceBinding[];
dataSource?: { key: "combined-custom-charts"; chartIndex?: number };
};
type LegacySliceBinding = {
order: number; // matches element.order in layout
source: LegacySliceBindingSource;
};Binding source kinds
source.kind | Used for | Description |
|---|---|---|
layout_only | legend, spanning_lines | Data comes from layout fields only |
area_series | area (multi-series) | Maps each layout series[].id to a v1 dataset name |
dataset | area (single), scatter, ranges_lane, single event lane | One v1 dataset by datasetName |
multi_dataset | events_lane | Maps each eventSeries[].id to a v1 dataset name |
static | static | Inline { value: unknown } (e.g. StatsCard payload) |
Example manifest (Storybook / tests only)
Source: ui/dashboard/src/components/organisms/composable-chart/legacy/fixtures/terrainStory.fixture.ts (terrainTimelineStoryManifest). Not exported from heat-next.
{
"layout": {
"anchor": "time",
"xAxis": { "ticks": [0, 300000, 600000, 900000] },
"elements": [
{ "type": "legend", "order": 0, "height": 40, "legendItems": [] },
{ "type": "area", "order": 1, "height": 160, "series": [
{ "id": "speed", "label": "Speed", "color": "#81842C", "render": "line_gradient", "unit": " km/h" },
{ "id": "elevation", "label": "Elevation", "color": "#999999", "render": "step_area", "unit": " m" }
]},
{ "type": "spanning_lines", "order": 2, "times": [198000, 630000, 639000], "style": { "color": "#C13D40", "visibilityKey": "hits" } },
{ "type": "ranges_lane", "order": 3, "label": "Comms", "height": 32 },
{ "type": "events_lane", "order": 4, "label": "Commander", "height": 32, "eventSeries": [
{ "id": "cmdr_rep", "label": "Reports" },
{ "id": "cmdr_cmd", "label": "Commands" }
]}
]
},
"bindings": [
{ "order": 0, "source": { "kind": "layout_only" } },
{
"order": 1,
"source": {
"kind": "area_series",
"series": [
{ "seriesId": "speed", "datasetName": "Air Speed", "datasetType": "area" },
{ "seriesId": "elevation", "datasetName": "Altitude", "datasetType": "area" }
]
}
},
{ "order": 2, "source": { "kind": "layout_only" } },
{ "order": 3, "source": { "kind": "dataset", "datasetName": "Comms Events", "datasetType": "rangeBar" } },
{
"order": 4,
"source": {
"kind": "multi_dataset",
"datasets": {
"cmdr_rep": "Commander Reports",
"cmdr_cmd": "Commander Commands"
}
}
}
],
"dataSource": { "key": "combined-custom-charts", "chartIndex": 0 }
}Slice layout fields (colors, y-axis bands, legend items, spanning times) mirror the Next ComposableChart reference Storybook terrain example.
Step-by-step: implement a custom legacy chart
- Runner output , Ensure
combined-custom-chartsincludes datasets with stablenameand correcttype(area,scatter1,rangeBar). - Design layout , Copy slice structure from the Next doc or terrain reference; assign unique
orderper slice. - Write bindings , One binding per slice that needs v1 data; use
layout_onlyfor legend and spanning lines whose times live in layout. - Embed manifest , Add
configuration.composableChartLegacyManifeston the layout col (required for legacy UI). - Mount widget , Set
"component": "ComposableChart"inlayoutConfiguration. - Verify , Load session in
ui/legacy; check resolver warnings in dev builds; compare with Storybook terrain mock.
See also: ComposableChart custom legacy guide.
Resolver API
Exported from heat-next:
import {
resolveLegacyComposableChart,
ComposableChartLegacy,
type ComposableChartLegacyManifest,
} from "heat-next";
const { slices, xDomain, xAxis, anchor, warnings } = resolveLegacyComposableChart({
manifest,
payload: combinedCustomCharts,
timeOriginMs: optionalOverride,
});| Result field | Description |
|---|---|
slices | ComposableChartResolvedSlice[] ready for ComposableChart |
xDomain | Session-relative [min, max] ms from data extent |
xAxis, anchor | Passed through from manifest layout |
warnings | Non-fatal issues (missing dataset, missing binding, unsupported slice) |
Slice binding reference
legend , layout_only
Provide legendItems on the layout element. No v1 dataset.
area , area_series or dataset
- Multi-series:
area_serieswithseriesIdmatching layoutseries[].id. - Single series:
datasetbinding; layout may omitseriesarray.
spanning_lines , layout_only
Set element.times in session-relative milliseconds (same units as resolved x-domain). Optional style.color and style.visibilityKey for legend toggle.
ranges_lane , dataset
Bind to a v1 dataset with type: "rangeBar".
events_lane , multi_dataset or dataset
- Multiple series:
multi_datasetkeys must matcheventSeries[].id. - Single series:
datasetwithscatter1type.
scatter , dataset
Maps continuous v1 series points to scatter positions.
static , static
{ "order": 7, "source": { "kind": "static", "value": { "statsCard": [] } } }playback , not supported
If present in layout, resolver emits a warning and the slice shows the standard “Not ready for use” placeholder. Use the widget’s local playback bar (legacy) or page-level controls on v2.