Skip to Content
This documentation is provided with the HEAT environment and describes this HEAT instance.
DevelopersDashboardsLegacy dashboardsCustom ComposableChart recipe

Custom ComposableChart on Legacy dashboards

This guide walks through adding a custom composable chart to a Legacy session detail page. For v2 / dashboard-v2 pages, use ComposableChart (Next) instead.

Prerequisites

  • Legacy dashboard node output includes combined-custom-charts with named datasets
  • Layout uses mount id ComposableChart
  • heat-next build includes ComposableChartLegacy (run npm run build:lib in ui/dashboard when developing locally)

1. Shape runner output

Publish datasets the resolver understands:

{ "combined-custom-charts": [ { "title": "My chart", "datasets": [ { "name": "Speed", "type": "area", "unit": "km/h", "data": [ { "DateTime": "2025-02-17T15:36:37.000Z", "Data": 42 } ] }, { "name": "Comms window", "type": "rangeBar", "data": [ { "StartTime": "2025-02-17T15:37:00.000Z", "EndTime": "2025-02-17T15:38:30.000Z" } ] }, { "name": "Report events", "type": "scatter1", "data": [ { "DateTime": "2025-02-17T15:40:00.000Z", "Data": 1 } ] } ] } ] }

Dataset name values must match your manifest bindings exactly (case-insensitive match).

2. Author the legacy manifest

Start from the terrain reference:

  • TypeScript (Storybook/tests only): terrainTimelineStoryManifest in ui/dashboard/src/components/organisms/composable-chart/legacy/fixtures/terrainStory.fixture.ts
  • Docs: ComposableChart (Legacy) manifest section

Minimal example:

{ "layout": { "anchor": "time", "elements": [ { "type": "area", "order": 0, "height": 160, "series": [{ "id": "speed", "label": "Speed", "color": "#81842C" }] }, { "type": "ranges_lane", "order": 1, "label": "Comms", "height": 32 }, { "type": "events_lane", "order": 2, "label": "Reports", "height": 32, "eventSeries": [{ "id": "reports", "label": "Reports", "color": "#666" }] } ] }, "bindings": [ { "order": 0, "source": { "kind": "area_series", "series": [{ "seriesId": "speed", "datasetName": "Speed", "datasetType": "area" }] } }, { "order": 1, "source": { "kind": "dataset", "datasetName": "Comms window", "datasetType": "rangeBar" } }, { "order": 2, "source": { "kind": "multi_dataset", "datasets": { "reports": "Report events" } } } ], "dataSource": { "key": "combined-custom-charts", "chartIndex": 0 } }

3. Embed manifest in layout

Add the manifest to the layout column configuration:

{ "layoutConfiguration": [ { "cols": [ { "component": "ComposableChart", "colspan": 12, "marginBottom": 24, "configuration": { "composableChartLegacyManifest": { } } } ] } ] }

Replace { } with your manifest JSON. If omitted, the SPA wrapper uses the terrain reference manifest (demo only).

4. Verify in ui/legacy

  1. Deploy or load a session whose dashboard layout includes ComposableChart.
  2. Confirm combined-custom-charts loads (same path as TimelineChart).
  3. Check for resolver warnings (shown inline in dev when datasets are missing).
  4. Exercise local playback: scrubber should move the yellow cursor when anchor is "time".

5. Optional: unit test your manifest

In ui/dashboard, resolve against a fixture payload:

import { resolveLegacyComposableChart } from "@/components/organisms/composable-chart/legacy"; import { buildTerrainLegacyPayload } from "@/components/organisms/composable-chart/legacy/fixtures/terrainLegacyPayload.fixture"; const result = resolveLegacyComposableChart({ manifest: myManifest, payload: buildTerrainLegacyPayload(), }); expect(result.warnings).toEqual([]); expect(result.slices.length).toBeGreaterThan(0);

6. Storybook reference

Compare with TerrainTimelineMock in ui/dashboard Storybook (Organisms/ComposableChart). Legacy binding should visually match when dataset names and layout align with the terrain reference.

Limitations reminder

  • No $heat-dataservice or TimelineClock sync with map playback
  • No in-chart playback slice (placeholder only)
  • Spanning line times must be session-relative ms in layout, not v1 DateTime strings
Opens your browser's print dialog. Choose "Save as PDF" as the destination.