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

CustomRenderer (Next)

⚠️

CustomRenderer is an escape hatch, not a product default.

  • Prefer a built-in layout component (MapDisplay, ComposableChart, StatsSummary, and so on) wired to $heat-dataservice channels from the dashboard-v2 node (or the Legacy dashboard facade on v1).
  • Use CustomRenderer only for short-lived prototypes, one-off operator layouts, or migration bridges while a proper node template and widget exist.
  • In production, use renderMode: "sandbox" (the default). Do not use inline unless the template is fully trusted and you accept full access to the parent dashboard page (tokens, DOM, storage).
  • Custom HTML/JS is not reviewed by HEAT at runtime: template authors are trusted, but mistakes can break the session page or leak data in inline mode.

CustomRenderer renders arbitrary HTML, CSS, and JS from layout JSON (customRendererItem). It does not subscribe to $heat-dataservice channels the way MapDisplayDS or ComposableChartDS do.

On session analysis pages, scripts can read window.$session, including inside the sandboxed iframe (see Does $session work in sandbox?).

When to use something else

Your goalRecommended approach
Time-series chart, map, stats, playbackBuilt-in component + channels per dashboard-v2 upstream contract
Stable contract for integratorsNew or existing node template + runner output + documented channel shape
Read simulation metrics in the UIBind configuration.channels on a standard widget; let the data service handle sampling
Quick spike / internal demoCustomRenderer in sandbox, keep JS small, branch on $session.generation
Access parent dashboard auth or DOMAvoid CustomRenderer; if unavoidable, inline mode on trusted templates only (still discouraged)

If the dashboard-v2 node already publishes data, the sensible path is almost always: add or reuse channels in $heat-dataservice, then pick a manifest widget that declares the right channelShapes. CustomRenderer should not replace that pipeline.

How this relates to the dashboard node

The dashboard-v2 node (Legacy: dashboard node) does two jobs that matter here:

  1. Layout , merged layoutConfiguration (rows, component ids, per-widget configuration). This becomes $session.layout / $session.layoutConfiguration on the session page.
  2. Session payload , the JSON blob the runner stored for the dimension (channel data, scores, legacy keys, and so on). On the session page this is exposed as $session.payload without HEAT rewriting its shape.
DataBuilt-in widget (e.g. MapDisplay)CustomRenderer
Layout metadataconfiguration on the layout rowSame row’s configuration + customRendererItem
Metric / series data$heat-dataservice channels via useDataServiceData$session.payload only (opaque snapshot)
Playback clockPage / widget integration with data serviceNot on $session (use built-in widgets or inline + window.heat on Next)

Next (generation: "next"): $session.payload is the last raw session API object captured before channel ingest. It often contains $heat-dataservice (canonical realms/channels) but may still include legacy top-level keys if the runner publishes them. The data service also ingests channels for built-in widgets; CustomRenderer does not automatically use that ingest path.

Legacy (generation: "legacy"): $session.payload is the v1 dashboard facade blob (dashboardData: keys such as Map, combined-custom-charts, ImpactHeatmap, and so on). Built-in Legacy widgets still fetch their own API slices; CustomRenderer only sees what you read from $session.payload in script.

Rule of thumb: if the data you need is already a channel in $heat-dataservice, do not scrape it from $session.payload in CustomRenderer. Add a proper widget binding instead. Use $session.payload when you truly need ad hoc access to the stored runner JSON (debug panels, migration shims, prototypes).

Dashboard generation

Value
Frontendui/dashboard (CustomRendererDSCustomRenderer)
Legacyui/legacy thin wrapper importing CustomRenderer from heat-next (npm run prepare:heat-next in legacy after dashboard build:lib)
Channel bindingNone , channels: [] in layout
Session contextwindow.$session on session analysis routes

Layout component identifier

CustomRenderer , cols[].component in v2 layout rows (Legacy mount id matches).

Layout configuration

FieldDescription
component"CustomRenderer"
nameInternal widget name
titleContentPanel title (shown when useCard is true)
channelsUsually []
customRendererItemHTML/CSS/JS payload and render options

customRendererItem

FieldRequiredDefaultDescription
htmlyes,HTML fragment in the widget body
cssno""CSS rules injected into the render context
jsno""JavaScript run after HTML is mounted
useCardnotrueWhen true, wrap with title + ComponentContainer
renderModeno"sandbox""sandbox" or "inline" (see security)
heightno12rem min-heightCSS length for the render area
{ "component": "CustomRenderer", "colspan": 6, "configuration": { "name": "custom-demo", "titleContent": "Custom panel", "channels": [], "customRendererItem": { "html": "<div id=\"root\"></div>", "css": "#root { font-family: sans-serif; padding: 1rem; }", "js": "var r = document.getElementById('root'); r.textContent = $session.title + ' (' + $session.generation + ')';", "useCard": true, "renderMode": "sandbox" } } }

Inline example (discouraged except trusted internal templates)

{ "component": "CustomRenderer", "colspan": 12, "configuration": { "name": "custom-inline", "titleContent": "Trusted only", "channels": [], "customRendererItem": { "html": "<p id=\"x\">Inline</p>", "js": "document.getElementById('x').textContent = $session.query('scenarioInstance.scenarioName') || 'n/a';", "renderMode": "inline", "useCard": true } } }

Set useCard: false when the layout grid cell already provides chrome and you want edge-to-edge content.

window.$session

On session analysis routes, SessionPageContextProvider sets window.$session (Next and Legacy share the same top-level fields). CustomRenderer scripts read it without React hooks.

Does $session work in sandbox?

Yes. With the default renderMode: "sandbox", the dashboard builds the iframe srcDoc with a bootstrap script that runs before your css and js. That script assigns a frozen copy of $session on the iframe’s window, including query(path).

  • Your layout js runs inside the iframe and can use $session and $session.query(...) the same way as in inline mode.
  • The copy is a snapshot at iframe render time. When the user changes realm, or layout/payload updates, the iframe remounts with a new snapshot (not live postMessage yet).
  • The sandbox uses sandbox="allow-scripts" without allow-same-origin, so iframe script cannot read the parent dashboard DOM, cookies, or localStorage.

Inline mode uses the parent page’s window.$session directly (live reference, full page access).

API reference

FieldDescription
versionAlways 1
generation"next" or "legacy" , branch before assuming payload shape
dimensionIdRoute/query dimension or scenario instance id
titlePlain-text session title (simulation and scenario names)
sessionDate, fromTime, sessionDurationSame values as the session header
scenarioInstance{ scenarioName, from, to, scenarioInstanceId, id?, … } (ISO date strings when available)
dimensionSerialized dimension record on Next; usually null on Legacy
layoutFull layout object for the page
layoutConfigurationTop-level layout.configuration when present (playback flags, default realm, and so on)
selectedRealmActive realm on Next; null on Legacy
payloadOpaque runner/session blob (see below)
query(path)Dot-path lookup on the data object, e.g. query('layout.realms')

payload by generation (not normalized)

generationTypical payload contentsHow it was produced
legacyv1 facade object: Map, combined-custom-charts, ImpactHeatmap, …getDashboardData / dashboard node output for Legacy
nextRaw pre-ingest session JSONOften includes $heat-dataservice (version, groups, realms[] with channels[]) plus any other keys the runner stored

HEAT does not map Legacy keys into Next shapes inside $session.payload. Inspect defensively:

if ($session.generation === "next" && $session.payload && $session.payload["$heat-dataservice"]) { var realms = $session.payload["$heat-dataservice"].realms || []; // prototype-only: list realm names } else if ($session.generation === "legacy" && $session.payload) { var keys = Object.keys($session.payload); }

For production analytics UI, prefer layout channels + a built-in widget consuming the ingested data service, not manual payload parsing in CustomRenderer.

Practical authoring workflow

  1. Confirm a built-in widget cannot do the job (see When to use something else).
  2. Ship the dashboard node so layout and payload exist; open a session analysis page in the target generation (Next vs Legacy).
  3. Inspect in the browser console (same generation as your users):
    • $session.generation, $session.title, $session.query('layoutConfiguration')
    • Object.keys($session.payload || {})
    • On Next: $session.query('payload.$heat-dataservice.realms') if present
  4. Prototype in sandbox with small html / css / js in customRendererItem; keep renderMode: "sandbox".
  5. Harden or replace with a node template + channel + manifest widget before calling the layout production-ready.

Script examples

Header line from session metadata (sandbox or inline):

document.getElementById("hdr").textContent = $session.title + " , " + ($session.query("scenarioInstance.scenarioName") || "");

List realm names from stored Next payload (sandbox):

var ds = $session.query("payload.$heat-dataservice"); var names = (ds && ds.realms) ? ds.realms.map(function (r) { return r.name; }) : []; document.getElementById("realms").textContent = names.join(", ") || "(none)";

Read layout playback setting:

var enabled = $session.query("layoutConfiguration.enableGlobalPlaybackControls");

Limitations

TopicBehaviour
Sandbox snapshotIframe $session updates when layout, realm, or payload snapshot changes (iframe remount), not on every parent render tick
Payload sizeLarge payload in sandbox increases srcDoc size and load cost
PlaybackNo scrubber time on $session; built-in widgets sync via data service clocks
ChannelsCustomRenderer ignores configuration.channels; no useDataServiceData
ThemingSandbox iframe does not inherit dashboard CSS variables unless you copy rules into css
Air-gapNo external <script src="https://…">; inline all assets in layout JSON

Security matrix

Layout JSON is trusted-author input. There is no HTML sanitizer: isolation mode is the control.

renderModeWhere code runsParent dashboard accessRecommendation
sandbox (default)Sandboxed iframe srcDocIsolatedUse for any non-trivial custom UI
inlineParent document (innerHTML + new Function)Full (tokens, DOM, storage)Avoid; internal trusted templates only

Air-gapped deployments: embed CSS and JS in layout JSON. CDN scripts will not load without outbound network.

Inline mode logs a dev-only console warning. Script errors show an in-widget error state instead of crashing the whole session page.

Implementation notes

  • Session analysis pages wrap the layout grid with SessionPageContextProvider.
  • Sandbox srcDoc order: session bootstrap → csshtml body → your js.
  • Closing </script> in layout js is escaped in srcDoc so the document stays well-formed.
  • Prefer small layout JSON; large scripts are hard to review and slow to load.