Skip to content

Heatmaps

Render plugin-computed scalar data as a heatmap overlay. Accessed via snaptrude.analysis.heatmaps.

Unlike the built-in studies (Sunlight Hours, Illuminance) where the host computes the values, here the plugin brings its own numbers — occupancy, energy use, a CFD field, anything — and the host maps them through a colour ramp and paints them into the scene. Four render modes:

  • renderSpaces — one flat colour per space, painted on the space's top face (per-room metrics).
  • renderGrid — a coloured grid mesh from point samples in world coordinates (fields sampled over an area: wind, noise, microclimate).
  • renderSurfaceGrid — the same grid on an arbitrarily oriented plane (a façade, a section cut): cells oriented by a surface normal (solar irradiance, glare, panel yield, wind pressure on a face).
  • renderFieldarbitrary cell geometry: planar polygon cells (host-triangulated) or pre-tessellated meshes, one value each — analysis meshes straight from a solver (FEA/CFD cells, Voronoi regions, per-panel patches).

Shared semantics:

  • Named overlays — every render call targets a named overlay (options.name; a shared default overlay when omitted). Rendering an existing name replaces that overlay only; the newly rendered overlay becomes the visible one — exactly one overlay is visible at a time. Up to 16 overlays can be registered; manage them via overlays.
  • Colour rampoptions.colors is an ordered list of hex colour stops, low → high (e.g. ["#0000ff", "#ff0000"]); the host default is a blue → cyan → green → yellow → red ramp. Values outside [min, max] clamp to the end colours. min/max are auto-derived from the data when omitted.
  • Colour scaleoptions.scale picks how values become colours: discrete bands over the ramp (2–64, default 11 — the default when scale is omitted), binary threshold (pass/fail), or exact-match categorical classes. See Scales.
  • Legend — a client-drawn card (title, unit, discrete colour segments, min/max) with < / > markers when the data ran past the range.
  • Hover tooltip — pass options.hover: true to any render call and, while that overlay is visible, hovering a cell shows a tooltip with the cell's value (and its meta payload, when given). See Hover.
  • Ephemeral, in-session state — overlays live only in the current session: never persisted, never serialized, never saved with views, and all overlays are cleared together when a scene-mutating edit invalidates them (all-or-nothing staleness). Re-render after changing the model.

At a glance

MethodWhat it doesMutates?
renderSpaces(entries, options?)Flat colour per space, on each space's top face
renderGrid(cells, cellSize, options?)Coloured grid mesh from world-coordinate samples
renderSurfaceGrid(cells, cellSize, normal, options?)Grid on an oriented plane (façade/section)
renderField(cells, options?)Arbitrary polygon / pre-tessellated cells
reset()Remove every overlay and close the legend
isActive()Whether any plugin heatmap overlay exists
overlays.list()Every registered overlay (name, visible, kind)
overlays.show(name)Make an overlay the (single) visible one
overlays.hide(name)Hide an overlay without removing it
overlays.remove(name)Dispose an overlay's meshes and forget it
overlays.removeAll()Remove every overlay; returns the count

Types

PluginAnalysisHeatmapSpaceEntry

PropertyTypeDescription
spaceComponentHandleHandle of the space to colour
valuenumberThe scalar value mapped to a colour

PluginAnalysisHeatmapGridCell

PropertyTypeDescription
position{ x, y, z }Cell centre in world coordinates
valuenumberThe scalar value mapped to a colour
metaunknown | undefinedOptional payload stored with the overlay — shown in the hover tooltip

PluginAnalysisHeatmapFieldCell

One cell of renderField — either form, all coordinates in world coordinates (y up):

PropertyTypeDescription
polygon{ x, y, z }[]Planar ring of 3+ points (any orientation); the host triangulates it
valuenumberThe scalar value mapped to a colour
metaunknown | undefinedOptional payload stored with the overlay

or pre-tessellated:

PropertyTypeDescription
vertices{ x, y, z }[]Mesh vertices (3+)
indicesnumber[]Triangle list into vertices — length a multiple of 3, every index in range
valuenumberThe scalar value mapped to a colour
metaunknown | undefinedOptional payload stored with the overlay

PluginAnalysisHeatmapScale

A discriminated union on type — see Scales:

KindPropertyTypeDescription
bandscountnumber | undefinedNumber of discrete bands, 2–64 (default 11)
thresholdthresholdnumberValues >= threshold pass
thresholdpassColorstring | undefinedHex colour for passing values (host default green)
thresholdfailColorstring | undefinedHex colour for failing values (host default red)
thresholdpassLabelstring | undefinedLegend label for the pass swatch (default ≥ threshold)
thresholdfailLabelstring | undefinedLegend label for the fail swatch (default < threshold)
categoricalclasses{ value, label, color }[]Exact-match classes: value, legend label, hex colour

PluginAnalysisHeatmapOptions

PropertyTypeDescription
namestring | undefinedOverlay name to render into (1–64 chars); a shared default overlay when omitted
titlestring | undefinedLegend title
unitstring | undefinedUnit label shown on the legend (e.g. "m/s")
minnumber | undefinedValue mapped to the first colour stop; data minimum when omitted
maxnumber | undefinedValue mapped to the last colour stop; data maximum when omitted
colorsstring[] | undefinedOrdered hex colour stops, low → high; host blue → cyan → green → yellow → red default
scalePluginAnalysisHeatmapScale | undefinedColour scale: bands (default), threshold, or categorical
hoverboolean | undefinedPer-cell hover tooltip while this overlay is visible. Default false

PluginAnalysisHeatmapsRenderResult

PropertyTypeDescription
successtrueThe heatmap is rendered on the scene

Render failures throw, so a returned value always carries success: true.

Functions

renderSpaces(entries, options?)

Render a per-space heatmap — one flat colour per space's top face. Each entry maps a space to a scalar value; the value is mapped through the colour ramp and the space's top face is painted that single flat colour. Renders into the overlay named options.name (the default overlay when omitted), replacing that overlay only and making it the visible one. Spaces whose top face is not flat (pitched/sloped tops) are skipped with a console warning — the rest of the heatmap still renders.

  • Parameters:
    • entries: PluginAnalysisHeatmapSpaceEntry[] — one per space.
    • options: PluginAnalysisHeatmapOptions | undefined
  • Returns: PluginAnalysisHeatmapsRenderResult{ success: true }.
  • Throws: When entries is empty, a handle does not resolve to a space, no space had a flat top face to paint, options.colors has a malformed hex, options.min is greater than options.max, options.name is a new overlay name while the 16-overlay cap is reached, or plugin writes are disabled.
ts
const { groups } = await snaptrude.program.areas.list("storeys");
const { members } = await snaptrude.program.areas.listMembers("storeys", groups[0].groupId);
await snaptrude.analysis.heatmaps.renderSpaces(
  members.map((m) => ({ space: m.id, value: occupancy[m.id] ?? 0 })),
  { title: "Occupancy", unit: "people", min: 0, max: 50 }
);

renderGrid(cells, cellSize, options?)

Render a grid heatmap — a coloured mesh from point samples. Each cell is a scalar sample at a world-coordinate position (y is up — a ground-level field sits at y = 0); the host builds one square cell of edge cellSize per sample — in the same Snaptrude internal units as position (convert real-world lengths via core.units.convert) — coloured through the ramp. This is the mode for field data — wind, noise, microclimate. Renders into the overlay named options.name (the default overlay when omitted), replacing that overlay only and making it the visible one.

  • Parameters:
    • cells: PluginAnalysisHeatmapGridCell[] — one per sample.
    • cellSize: number — edge length of each square cell, in the same Snaptrude internal units as position (convert via core.units.convert). Must be a positive, finite number.
    • options: PluginAnalysisHeatmapOptions | undefined
  • Returns: PluginAnalysisHeatmapsRenderResult{ success: true }.
  • Throws: When cells is empty, cellSize is not a positive, finite number, options.colors has a malformed hex, options.min is greater than options.max, options.name is a new overlay name while the 16-overlay cap is reached, or plugin writes are disabled.
ts
// World coordinates are y-up: a ground-level field varies in x/z at y = 0.
await snaptrude.analysis.heatmaps.renderGrid(
  samples.map((s) => ({ position: { x: s.x, y: 0, z: s.z }, value: s.windSpeed })),
  2, // 2-unit square cells
  { title: "Wind speed", unit: "m/s", colors: ["#0000ff", "#00ff00", "#ff0000"] }
);

renderSurfaceGrid(cells, cellSize, normal, options?)

Render a grid heatmap on an arbitrarily oriented plane — a façade, a section cut, any flat surface. The same point-sample model as renderGrid, generalised: cells lie in the plane whose direction is given by normal (for a façade, the outward wall normal — read it via core.geom.query.face.getNormal). Each square cell is centred on its world-coordinate position, oriented in that plane, and lifted a hair along the normal so the heatmap never z-fights the surface it annotates. Positions should lie on (or very near) the surface — the host does not project them. renderGrid is exactly this call with normal = { x: 0, y: 1, z: 0 }. Renders into the overlay named options.name (the default overlay when omitted), replacing that overlay only and making it the visible one.

This is the mode for per-façade data: solar irradiance, glare, panel-level PV yield, wind pressure on an elevation.

  • Parameters:
    • cells: PluginAnalysisHeatmapGridCell[] — one per sample, positions on the surface plane.
    • cellSize: number — edge length of each square cell, in the same Snaptrude internal units as position (convert via core.units.convert). Must be a positive, finite number.
    • normal: { x, y, z } — the plane's normal direction; any non-zero length (the host normalises).
    • options: PluginAnalysisHeatmapOptions | undefined
  • Returns: PluginAnalysisHeatmapsRenderResult{ success: true }.
  • Throws: When cells is empty, cellSize is not a positive finite number, normal has zero length, options.colors has a malformed hex, options.min is greater than options.max, options.name is a new overlay name while the 16-overlay cap is reached, or plugin writes are disabled.
ts
// Samples on a wall plane facing +X; 1-unit cells coloured by irradiance.
await snaptrude.analysis.heatmaps.renderSurfaceGrid(
  samples.map((s) => ({ position: { x: wallX, y: s.y, z: s.z }, value: s.irradiance })),
  1,
  { x: 1, y: 0, z: 0 }, // façade normal
  { title: "Irradiance", unit: "kWh/m²", colors: ["#0000ff", "#ffff00", "#ff0000"] }
);

renderField(cells, options?)

Render a heatmap from arbitrary cell geometry — the free-form sibling of the grid calls. Each PluginAnalysisHeatmapFieldCell brings its own footprint in world coordinates: either a planar polygon ring (3+ points, any orientation — the host projects it onto its best-fit plane and triangulates with earcut) or a pre-tessellated vertices + indices triangle mesh straight from a solver. Values map through the colour scale like every other render call. Degenerate polygon cells (collinear, zero area) are skipped with a console warning; an optional per-cell meta payload is stored with the overlay. Renders into the overlay named options.name (the default overlay when omitted), replacing that overlay only and making it the visible one.

This is the mode for analysis meshes that are not square grids: FEA/CFD cells, Voronoi regions, per-panel façade patches, zone polygons.

  • Parameters:
    • cells: PluginAnalysisHeatmapFieldCell[] — one per cell (polygon or pre-tessellated form).
    • options: PluginAnalysisHeatmapOptions | undefined — including scale.
  • Returns: PluginAnalysisHeatmapsRenderResult{ success: true }.
  • Throws: When cells is empty, a polygon has fewer than 3 points, a pre-tessellated cell's indices length is not a multiple of 3 or an index is out of range, every polygon cell was degenerate, a categorical scale has no class for a cell's value, a colour is malformed, options.min is greater than options.max, options.name is a new overlay name while the 16-overlay cap is reached, or plugin writes are disabled.
ts
// Two triangular solver cells at ground level, pass/fail at 300 lux.
await snaptrude.analysis.heatmaps.renderField(
  [
    { polygon: [{ x: 0, y: 0, z: 0 }, { x: 4, y: 0, z: 0 }, { x: 0, y: 0, z: 4 }], value: 420 },
    { polygon: [{ x: 4, y: 0, z: 4 }, { x: 0, y: 0, z: 4 }, { x: 4, y: 0, z: 0 }], value: 180 }
  ],
  { title: "Daylight", unit: "lux", scale: { type: "threshold", threshold: 300 } }
);

Scales

options.scale (all render calls) picks how values become colours and how the legend reads:

  • { type: "bands", count? } — the default (omitting scale is bands with count: 11): the colour ramp (options.colors) quantised into count discrete bands, 2–64. The legend shows the band swatches with the numeric min–max range.
  • { type: "threshold", threshold, passColor?, failColor?, passLabel?, failLabel? } — binary pass/fail: values >= threshold get the pass colour (host default green), the rest the fail colour (host default red). The legend shows the two labelled swatches (labels default to < threshold / ≥ threshold).
  • { type: "categorical", classes } — exact-match classes: every cell value must equal one class's value — anything else is a validation error listing the offending values. The legend shows one labelled swatch per class.

Discrete bands, by design

The heatmap meshes are shaded with one flat colour per band — the WebGPU render pipeline ignores vertex colours, so a smooth per-vertex gradient is not possible. "Continuous" is simply a high band count: up to 64 bands, which reads as continuous at typical zoom levels.

ts
// 64-band "continuous" look:
await snaptrude.analysis.heatmaps.renderGrid(cells, 2, { scale: { type: "bands", count: 64 } });

// Category colouring:
await snaptrude.analysis.heatmaps.renderField(cells, {
  scale: {
    type: "categorical",
    classes: [
      { value: 0, label: "Office", color: "#4477aa" },
      { value: 1, label: "Retail", color: "#ee6677" },
      { value: 2, label: "Lobby", color: "#ccbb44" }
    ]
  }
});

Hover

Pass hover: true in any render call's options to get a per-cell tooltip: while that overlay is the visible one, moving the cursor over a cell shows the cell's value (with the legend unit, when set) — and, when the cell carries a meta payload, its stringified content alongside. Moving off the overlay hides the tooltip.

  • Works for every render mode — spaces, grid, surface grid and field cells alike.
  • meta is accepted on grid/surface-grid cells and field cells; space entries show just their value.
  • Hover is per overlay: it follows the overlay through overlays.show / hide, and switching to an overlay rendered without hover turns the tooltip off.
  • The overlay meshes stay invisible to selection — hovering or clicking through a heatmap never selects it.
ts
await snaptrude.analysis.heatmaps.renderGrid(
  samples.map((s) => ({
    position: { x: s.x, y: 0, z: s.z },
    value: s.windSpeed,
    meta: { gust: s.gust, direction: s.direction }
  })),
  2,
  { title: "Wind speed", unit: "m/s", hover: true }
);
// Hovering a cell now shows e.g.  7.4 m/s — {"gust":11.2,"direction":"SW"}

reset()

Clear every plugin heatmap overlay from the scene and close the legend — equivalent to overlays.removeAll() under its historical name. A no-op (returns false) when no plugin heatmap exists. Does not touch the built-in study heatmaps (sunlightHours / illuminance) — they have their own reset.

  • Returns: booleantrue when at least one overlay was cleared, false when none existed.
  • Throws: When plugin writes are disabled.
ts
await snaptrude.analysis.heatmaps.reset();

isActive()

Whether any plugin heatmap overlay exists — true between a successful render and the next reset / overlays.removeAll (or the scene-mutating edit that auto-clears everything). A pure read.

  • Returns: booleantrue when a plugin heatmap overlay is on the scene (visible or hidden).
ts
if (!(await snaptrude.analysis.heatmaps.isActive())) {
  await snaptrude.analysis.heatmaps.renderGrid(cells, 2);
}

Overlays

Manage the named overlays created by the render calls. Accessed via snaptrude.analysis.heatmaps.overlays.

Every render registers (or replaces) an overlay under options.name — a shared default overlay when omitted, so unnamed renders keep the classic replace-in-place behaviour. Overlays are in-session only (never persisted or serialized), at most 16 can be registered at once (rendering a 17th new name throws — remove one first), and exactly one is visible at a time: showing one hides the others, and the legend always describes the visible overlay (nothing visible → no legend). A scene-mutating edit clears all overlays together.

overlays.list()

Every registered overlay. At most one has visible: true.

  • Returns: { overlays: { name: string; visible: boolean; kind: "spaces" | "grid" | "surfaceGrid" | "field" }[] }kind is which render call produced the overlay.
ts
const { overlays } = await snaptrude.analysis.heatmaps.overlays.list();
const visible = overlays.find((o) => o.visible);

overlays.show(name)

Make the named overlay the visible one — enables its meshes, hides whichever overlay was visible, and swaps the legend to its payload.

  • Returns: booleantrue when the overlay is now visible, false when no overlay of that name exists.
  • Throws: When plugin writes are disabled.
ts
await snaptrude.analysis.heatmaps.overlays.show("wind");

overlays.hide(name)

Hide the named overlay without removing it — it keeps its slot and can be brought back with show. Hiding the visible overlay closes the legend.

  • Returns: booleantrue when the overlay exists (and is now hidden), false when no overlay of that name exists.
  • Throws: When plugin writes are disabled.
ts
await snaptrude.analysis.heatmaps.overlays.hide("wind");

overlays.remove(name)

Dispose the named overlay's meshes and forget it, freeing its slot toward the 16-overlay cap. Removing the visible overlay closes the legend.

  • Returns: booleantrue when the overlay was removed, false when no overlay of that name exists.
  • Throws: When plugin writes are disabled.
ts
await snaptrude.analysis.heatmaps.overlays.remove("wind");

overlays.removeAll()

Remove every registered overlay and close the legend — the bulk form of remove (and what reset() does).

  • Returns: number — how many overlays were removed (0 when none existed).
  • Throws: When plugin writes are disabled.
ts
const removed = await snaptrude.analysis.heatmaps.overlays.removeAll();

Example — comparing two studies

ts
// Render two named overlays; the last rendered is visible.
await snaptrude.analysis.heatmaps.renderGrid(windCells, 2, { name: "wind", title: "Wind", unit: "m/s" });
await snaptrude.analysis.heatmaps.renderGrid(noiseCells, 2, { name: "noise", title: "Noise", unit: "dB" });

// Flip between them without re-computing.
await snaptrude.analysis.heatmaps.overlays.show("wind");
await snaptrude.analysis.heatmaps.overlays.show("noise");

// Done comparing.
await snaptrude.analysis.heatmaps.overlays.removeAll();