Appearance
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).renderField— arbitrary 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 viaoverlays. - Colour ramp —
options.colorsis 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/maxare auto-derived from the data when omitted. - Colour scale —
options.scalepicks how values become colours: discretebandsover the ramp (2–64, default 11 — the default whenscaleis omitted), binarythreshold(pass/fail), or exact-matchcategoricalclasses. 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: trueto any render call and, while that overlay is visible, hovering a cell shows a tooltip with the cell'svalue(and itsmetapayload, 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
| Method | What it does | Mutates? |
|---|---|---|
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
| Property | Type | Description |
|---|---|---|
space | ComponentHandle | Handle of the space to colour |
value | number | The scalar value mapped to a colour |
PluginAnalysisHeatmapGridCell
| Property | Type | Description |
|---|---|---|
position | { x, y, z } | Cell centre in world coordinates |
value | number | The scalar value mapped to a colour |
meta | unknown | undefined | Optional payload stored with the overlay — shown in the hover tooltip |
PluginAnalysisHeatmapFieldCell
One cell of renderField — either form, all coordinates in world coordinates (y up):
| Property | Type | Description |
|---|---|---|
polygon | { x, y, z }[] | Planar ring of 3+ points (any orientation); the host triangulates it |
value | number | The scalar value mapped to a colour |
meta | unknown | undefined | Optional payload stored with the overlay |
or pre-tessellated:
| Property | Type | Description |
|---|---|---|
vertices | { x, y, z }[] | Mesh vertices (3+) |
indices | number[] | Triangle list into vertices — length a multiple of 3, every index in range |
value | number | The scalar value mapped to a colour |
meta | unknown | undefined | Optional payload stored with the overlay |
PluginAnalysisHeatmapScale
A discriminated union on type — see Scales:
| Kind | Property | Type | Description |
|---|---|---|---|
bands | count | number | undefined | Number of discrete bands, 2–64 (default 11) |
threshold | threshold | number | Values >= threshold pass |
threshold | passColor | string | undefined | Hex colour for passing values (host default green) |
threshold | failColor | string | undefined | Hex colour for failing values (host default red) |
threshold | passLabel | string | undefined | Legend label for the pass swatch (default ≥ threshold) |
threshold | failLabel | string | undefined | Legend label for the fail swatch (default < threshold) |
categorical | classes | { value, label, color }[] | Exact-match classes: value, legend label, hex colour |
PluginAnalysisHeatmapOptions
| Property | Type | Description |
|---|---|---|
name | string | undefined | Overlay name to render into (1–64 chars); a shared default overlay when omitted |
title | string | undefined | Legend title |
unit | string | undefined | Unit label shown on the legend (e.g. "m/s") |
min | number | undefined | Value mapped to the first colour stop; data minimum when omitted |
max | number | undefined | Value mapped to the last colour stop; data maximum when omitted |
colors | string[] | undefined | Ordered hex colour stops, low → high; host blue → cyan → green → yellow → red default |
scale | PluginAnalysisHeatmapScale | undefined | Colour scale: bands (default), threshold, or categorical |
hover | boolean | undefined | Per-cell hover tooltip while this overlay is visible. Default false |
PluginAnalysisHeatmapsRenderResult
| Property | Type | Description |
|---|---|---|
success | true | The 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
entriesis empty, a handle does not resolve to a space, no space had a flat top face to paint,options.colorshas a malformed hex,options.minis greater thanoptions.max,options.nameis 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 asposition(convert viacore.units.convert). Must be a positive, finite number.options:PluginAnalysisHeatmapOptions | undefined
- Returns:
PluginAnalysisHeatmapsRenderResult—{ success: true }. - Throws: When
cellsis empty,cellSizeis not a positive, finite number,options.colorshas a malformed hex,options.minis greater thanoptions.max,options.nameis 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 asposition(convert viacore.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
cellsis empty,cellSizeis not a positive finite number,normalhas zero length,options.colorshas a malformed hex,options.minis greater thanoptions.max,options.nameis 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— includingscale.
- Returns:
PluginAnalysisHeatmapsRenderResult—{ success: true }. - Throws: When
cellsis empty, a polygon has fewer than 3 points, a pre-tessellated cell'sindiceslength is not a multiple of 3 or an index is out of range, every polygon cell was degenerate, acategoricalscale has no class for a cell's value, a colour is malformed,options.minis greater thanoptions.max,options.nameis 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 (omittingscaleisbandswithcount: 11): the colour ramp (options.colors) quantised intocountdiscrete 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>= thresholdget 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'svalue— 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.
metais 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 withouthoverturns 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:
boolean—truewhen at least one overlay was cleared,falsewhen 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:
boolean—truewhen 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" }[] }—kindis 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:
boolean—truewhen the overlay is now visible,falsewhen 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:
boolean—truewhen the overlay exists (and is now hidden),falsewhen 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:
boolean—truewhen the overlay was removed,falsewhen 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 (0when 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();