Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Purpose

Dashboard Canvas displays are the paradigm for data cards, not diagrams. Rows and columns of gauges, trends, KPIs, tables. Every element lives in a Cell, and cells are arranged in a responsive grid that reflows on window resize.

Use Dashboard when:

  • The display is primarily data monitoring (live values, trends, alarms)
  • The layout is a grid of cards — fleet status, shift summary, KPI wall, operator console
  • The screen must reflow across window sizes
  • There's no spatial/physical relationship between equipment to preserve

pixel art that happens to show live data. Process diagrams, P&IDs, equipment layouts, architectural overviews. Every element has an explicit Left / Top / Width / Height and you compose by placement, layering, and grouping.

Use Canvas when:

  • The display shows equipment in a spatial layout (pipe A connects to pump B connects to tank C)
  • The visual language is P&ID / process flow (operators scan it as a schematic)
  • Custom shapes and line work matter
  • You need the full set of dynamics (RotateDynamic, ScaleDynamic, MoveDragDynamic, SkewDynamic, BargraphDynamic are Canvas-only)

Use Dashboard Use Canvas (load Skill Display Construction — CanvasDashboard instead) when equipment positions relative to each other matter (pipe A connects to vessel B), you're building a P&ID, or you need animation dynamicsthe display is primarily data monitoring, grid-based cards, or there's no spatial relationship to preserve.

Prerequisite: load Skill Display Construction — Basics first. It covers theme-first thinking, write mechanics, the build loop, binding syntax, and spacing/typography tokens.

Section 1 —

...

Canvas mental model

A Dashboard is a grid of cells

The whole display is defined by:

  1. A DashboardDisplay root with Columns and Rows arrays describing the grid track sizes
  2. A Cells array, where each cell specifies Row, Col, optional RowSpan/ColSpan, a Cell.HeaderLink for the card title, and Content (the element that fills the cell)
Code Block
languagejson
{
  "Name": "OperationsOverview",
  "PanelType": "Dashboard",
  "DashboardDisplay": {
    "Columns": ["*", "*", "*"],
    "Rows": ["Auto", "*", "*"],
    "Cells": [
      { "Row": 0, "Col": 0, "ColSpan": 3, "Cell.HeaderLink": "Plant Overview", "Content": { /* header card */ } },
      { "Row": 1, "Col": 0, "Cell.HeaderLink": "Production Rate",  "Content": { /* gauge */ } },
      { "Row": 1, "Col": 1, "Cell.HeaderLink": "Active Alarms",    "Content": { /* alarm viewer */ } },
      { "Row": 1, "Col": 2, "Cell.HeaderLink": "Shift Output",     "Content": { /* KPI */ } }
    ]
  }
}

Track sizing

The Columns and Rows arrays define track sizes. Each entry can be:

  • "*" — equal share of remaining space (like CSS 1fr)
  • "2*" — twice the share (like CSS 2fr)
  • "Auto" — size to content
  • "240" — fixed pixel width
  • "*,min=200" — fractional with a minimum in pixels

Standard patterns:

Layout

Columns

Rows

3 equal columns, 2 rows

["*","*","*"]

["*","*"]

Header + 2 equal rows

["*","*","*"]

["Auto","*","*"]

Sidebar + main

["240","*"]

["*"]

Header + sidebar + main

["240","*"]

["Auto","*"]

4 KPIs across top, trend below

["*","*","*","*"]

["Auto","*"]

Operator wall (4×3)

["*","*","*","*"]

["*","*","*"]

Don't think in pixels — think in cells

In Canvas you place a gauge at Left: 472, Top: 320. In Dashboard you place a gauge at Row: 1, Col: 2. The engine handles pixel positioning, cell padding, and reflow.

This is a completely different mental model. If you find yourself calculating Left/Top/Width/Height for Dashboard elements, stop — you're building a Canvas display by accident.

Dashboard-compatible controls only

Not every element type works inside a Dashboard cell. Rule of thumb:

Works in Dashboard

? Interaction, Charts, Gauges, Viewer, Editors, TextBlock, Label

Does NOT work in Dashboard

? Shape primitives (Rectangle, Ellipse, Polygon, Path, Gridline), first-class auto-shapes (Cylinder, Gear, etc.), containers (ShapeGroup, SvgGroup, Group) — all Canvas-only

Dynamics work in Dashboard for visual controls (FillColorDynamic on a TextBlock, VisibilityDynamic on a chart) but the animation dynamics (Rotate, Scale, MoveDrag, Skew, Bargraph) are Canvas-only — they're silently ignored in Dashboard cells.

Section 2 — Standard grid recipes

3×2 KPI wall (the most common)

Six data cards in a 3-column × 2-row grid:

...

languagejson

...

Think in zones, not elements

The worst Canvas displays are built element-first — you place a Rectangle, then another, then wonder why nothing aligns. The good ones are built zone-first.

  1. Divide the canvas into zones. Rectangular regions for each process section (Intake, Treatment, Distribution) or each info panel (Equipment Detail, Live Metrics, Identity).
  2. Lay zone background Rectangles first. These are the visual scaffolding — operators read the display by scanning zones, not individual shapes.
  3. Place elements WITHIN zone coordinates. Everything in a zone has its Left/Top relative to the zone's origin.
  4. Connect zones with flow indicators. Arrows, pipe lines, direction markers.

Canvas sizes

CanvasWidth × HeightWhen
Standard HD1366 × 728Default, works everywhere
Wide1600 × 900Modern control-room monitors
Full HD1920 × 1080Dedicated operator displays
4K scaled3840 × 2160Rare — prefer 1920×1080 with StretchFill

Zone math (any canvas size, N zones)

margin   = 20
gap      = 15
titleBar = 60          // top strip for display title + status
bottomPanel = 140      // for trend/alarm/summary
zoneHeight = Height - titleBar - bottomPanel - margin
zoneWidth  = (Width - 2*margin - (N-1)*gap) / N
zone[i].Left  = margin + i * (zoneWidth + gap)
zone[i].Top   = titleBar
zone[i].Width = zoneWidth
zone[i].Height = zoneHeight

For a 1600×900 canvas with 3 zones: each zone is ~515 wide × 640 tall. For 4 zones: ~381 wide. For 2 zones: ~780 wide.

Z-order by Elements array order

Canvas has no z-index property. The order of the Elements array IS the z-order — earlier elements render behind, later elements in front. Always place:

  1. Full-canvas background Rectangle first (if overriding root Background)
  2. Zone background Rectangles next
  3. Zone-internal shapes and symbols
  4. Labels and text (on top of their containing zones)
  5. Interactive overlays (click-zones, hover highlights) last

Section 2 — Shapes (the 7 primitives)

All Canvas-only. All theme-aware (Fill/FillTheme, Stroke/StrokeTheme, StrokeThickness).

ShapeRequiredUse when
RectanglePanels, status bars, tank bodies, bars, backgrounds. Has RadiusX/RadiusY for rounded corners.
EllipseTank caps (top/bottom), status LEDs, sight glasses, pump bodies. Circle when Width=Height.
PolygonPoints ≥3Arrows, funnels, hoppers, diamonds. Auto-closes. Set Stretch: "None" to preserve exact point coords.
PolylinePoints ≥2Open curved lines. Doesn't close. Prefer Gridline for pipes.
PathDataAny curve, arc, complex shape. SVG mini-language (M L H V C Q A Z).
GridlinePoints ≥2Use for pipes and orthogonal connections. Constrained to horizontal/vertical segments. The P&ID convention.
SplinePoints ≥2Smooth curves through control points (Catmull-Rom). Rare — Path covers most cases.

Pipe segment pattern

{
  "Type": "Gridline",
  "Left": 300, "Top": 200,
  "Points": "0,0 100,0 100,60 200,60",
  "StrokeTheme": "Water",
  "StrokeThickness": 6,
  "StrokeLineJoin": "Round",
  "StrokeLineCap": "Round"
}

Then a direction arrow (Polygon, no stretch):

{
  "Type": "Polygon",
  "Left": 490, "Top": 253,
  "Width": 20, "Height": 14,
  "Points": "0,0 20,7 0,14",
  "Stretch": "None",
  "FillTheme": "Water"
}

Reactor coil overlay (Path)

Zigzag heat-exchange coils over a reactor body:

{
  "Type": "Path",
  "Left": 305, "Top": 220,
  "Width": 130, "Height": 240,
  "Data": "M0,0 Q65,20 130,0 M0,40 Q65,60 130,40 M0,80 Q65,100 130,80",
  "StrokeTheme": "StateRed",
  "StrokeThickness": 2,
  "Fill": "#00000000",
  "FillTheme": ""
}

Note the stroked-but-unfilled pattern: Fill: "#00000000" + FillTheme: "" gives a transparent fill so the stroke shows without a filled shape underneath.

Section 3 — First-class auto-shapes

These are first-class shape primitives that write with just Type + geometry + colors. The platform auto-injects the underlying Path/Polygon geometry on save. Massive productivity win over composing from primitives.

TypeDefault geometryUse for
CylinderVertical cylinder with elliptical capsTanks, vessels, drums, silos
Gear8-tooth gearMachinery icons, manual/auto toggles
ArrowRight-pointing arrowFlow direction, callouts (rotate via RotateDynamic for other directions)
CloudPuffy cloud outlineMQTT broker, cloud service, weather
Star5-pointed starFavorites, highlights, quality marker
HexagonRegular hexagonNode diagrams, honeycomb layouts
PentagonRegular pentagonRare — use for ANSI warning signs
TrapezoidIsosceles trapezoidHoppers, funnels, cone-bottom tank sections
{ "Type": "Cylinder", "Left": 300, "Top": 200, "Width": 80, "Height": 180, "FillTheme": "ElementBlue" }

That's an entire vessel. No Points, no Data, no compositing.

Writer normalization — read-back shows expanded form

When you read a display back after writing a Cylinder, you'll see a Path with auto-generated Data. This is expected — the shortcut is a write-time macro. For edits, either add new Cylinders (which normalize the same way) or edit the Path directly.

Runtime discovery over hardcoded lists

list_elements() is the authoritative runtime catalog. Any shape entry not returned by list_elements() in the current release should be treated as non-existent. For shapes beyond the standard set (e.g., triangle, octagon, custom forms), compose from primitives: Polygon with explicit Points handles most custom 2D shapes, and Path handles curves.

Section 4 — Containers (ShapeGroup, SvgGroup, Group)

ContainerChildrenDynamics apply toUse for
ShapeGroupShapes onlyALL children uniformly"The whole vessel turns alarm red when Running=0"
SvgGroupAuto-parsed from inline SVG stringALL children (normalized to ShapeGroup on write)"I have an SVG, I want dynamics per element"
GroupAny element typeEach child has independent dynamics"Interactive panel with chart+buttons that moves as a unit"

ShapeGroup — compose equipment with unified state

The killer feature: a FillColorDynamic on the ShapeGroup changes the fill of ALL children at once. Build a vessel from primitives, and the entire vessel can turn red on alarm:

{
  "Type": "ShapeGroup",
  "Left": 300, "Top": 200,
  "Width": 180, "Height": 220,
  "Stretch": "None",
  "Children": [
    { "Type": "Ellipse",   "Left": 40, "Top": 0,   "Width": 100, "Height": 20 },
    { "Type": "Rectangle", "Left": 40, "Top": 10,  "Width": 100, "Height": 180 },
    { "Type": "Ellipse",   "Left": 40, "Top": 180, "Width": 100, "Height": 20 }
  ],
  "FillTheme": "ElementBlue",
  "Dynamics": [
    {
      "Type": "FillColorDynamic",
   

...

   "LinkedValue": "@Tag.

...

Reactor/

...

Alarm",
    

...

  

...

4×3 operator wall

Twelve cells. Full-screen control-room overview with one card per reactor / line / zone:

...

"ChangeColorItems": [

...


        { "Type": "ChangeColorItem", "ChangeLimit": 0, "LimitColor": "#FF1E3A8A" },
        { "Type": "ChangeColorItem", "ChangeLimit": 1, "LimitColor": "#FFEF4444" }
      ]
    }
  ]
}

SvgGroup — when you'd rather author in SVG

Any inline SVG string becomes a ShapeGroup with native WPF shapes:

{
  "Type": "SvgGroup",
  "Left": 224, "Top": 240,
  "Width": 180, "Height": 220,
  "SvgContent": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 220'><ellipse cx='90' cy='14' rx='60' ry='10' fill='#95a5a6'/><rect x='30' y='14' width='120' height='180' fill='#3498db'/><ellipse cx='90' cy='194' rx='60' ry='10' fill='#95a5a6'/></svg>",
  "Name": "ReactorVessel"
}

Supported SVG elements: rect, circle, ellipse, line, path, polyline, polygon, g.
Supported attributes: fill, stroke, stroke-width, transform, style, class, opacity, rx, ry.
Supported transforms: translate(), scale(), rotate(), matrix().
Avoid: gradients, filters, clip-paths, text, embedded images.

Trade-off: SvgContent hex colors are opaque to the theme system. Fine for process-meaning colors (heat red, water blue), wrong for UI chrome. If you need theme-reactive composed equipment, use ShapeGroup directly.

Group — for interactive panels

Use Group when children need INDEPENDENT dynamics:

{
  "Type": "Group",
  "Left": 100, "Top": 100,
  "Width": 400, "Height": 280,
  "Children": [
    { "Type": "Rectangle", "Left": 0, "Top": 0, "Width": 400, "Height": 280, "FillTheme": "PanelBackground" },
    { "Type": "TextBlock", "Left": 16, "Top": 16, "Width": 360, "Height": 24, "LinkedValue": "Reactor Control" },
    { "Type": "Button",    "Left": 16, "Top": 220, "Width": 120, "Height": 40, "LabelLink": "Start",
      "Dynamics": [{ "Type": "ActionDynamic", "MouseLeftButtonDown": { "Type": "DynamicActionInfo", "ActionType": "SetValue", "ObjectLink": "@Tag.Reactor/Running", "ObjectValueLink": 1 }}] }
  ]
}

Section 5 — The equipment cookbook

Recipe 1 — Vessel with jacket

The archetype: a cylindrical vessel with a heating jacket that changes color when the heater is running.

{ "Type": "Ellipse",   "Left": 300, "Top": 180, "Width": 96, "Height": 24, "FillTheme": "ElementGray" },
{ "Type": "Rectangle", "Left": 328, "Top": 150, "Width": 40, "Height": 36, "FillTheme": "ElementGray" },
{ "Type": "Cylinder",  "Left": 276, "Top": 200, "Width": 144, "Height": 300, "FillTheme": "ElementBlue" },
{ "Type": "Rectangle", "Left": 256, "Top": 280, "Width": 22, "Height": 180, "FillTheme": "OffFill",
  "Dynamics": [
    { "Type": "FillColorDynamic

2-column detail (list → detail)

Asset tree on the left, detail panel on the right:

Code Block
languagejson
{
  "DashboardDisplay": {
    "Columns": ["240", "*"],
    "Rows": ["Auto", "*"],
    "Cells": [
      { "Row": 0, "Col": 0, "ColSpan": 2, "Cell.HeaderLink": "Production Area A", "Content": { "Type": "TextBlock", "LinkedValue": "{@Tag.Site/Name} — {@Now}" } },
      { "Row": 1, "Col": 0, "Cell.HeaderLink": "Equipment",                        "Content": { "Type": "AssetsTree" } },
      { "Row": 1, "Col": 1, "Cell.HeaderLink": "{@Client.Context.AssetName}",      "Content": { "Type": "ChildDisplay", "DisplayLink": "EquipmentDetail" } }
    ]
  }
}

The AssetsTree has all its bindings pre-wired to @Client.Context.* by default — drop it in and navigation "just works."

ColSpan / RowSpan for asymmetric layouts

Code Block
languagejson
{ "Row": 0, "Col": 0, "ColSpan": 2, ... }   // cell spans columns 0 and 1
{ "Row": 1, "Col": 0, "RowSpan": 2, ... }   // cell spans rows 1 and 2

Use ColSpan on a header card to stretch it across all grid columns. Use RowSpan when you want a tall element (AlarmViewer, AssetsTree) next to shorter cards.

Section 3 — Controls by cell purpose

Cell purpose

Best control

Why

Display title / section header

TextBlock with composite LinkedValue

"{@Tag.Site/Name} — {@Now}" in one element

Single-value KPI (numeric)

CenterValue

Pre-styled big-number-plus-unit tile

Single-value KPI (string)

TextBlock with FontSize 24+

Simpler than CenterValue for strings

Time-series trend

TrendChart

The workhorse

Tag value dial

RadialGauge

Gauge with threshold bands

Tag value bar (linear)

LinearGauge

For bar-style KPIs with setpoint pointer

Cumulative count / meter

DigitalGauge or DigitalMeter

Specialty numeric readouts

Categorical comparison

BarChart

e.g., output by shift

Current alarms

AlarmViewer

Pre-wired to Client.AlarmPage context

Asset hierarchy navigation

AssetsTree

Pre-wired to Client.Context

Dropdown selector

ComboBox (DataTable-backed for FK)

Zero-script FK lookup

Data table / list

DataGrid

The list in list→detail

Document viewer (SOP, work order)

PdfViewer

URL-bound inline PDF

Fleet/plant map

MapsOSM

Lat/long-positioned markers

Embedded another display

ChildDisplay

Reusable detail panels

Multiple panels user switches

TabControl

Manual click-to-switch

Slideshow of status boards

Carousel

Auto-cycle with AutoCycleLink

Collapsible panel

Expander

Click to expand advanced settings

Card-per-item layout

FlowPanel

N items from a data source, one template

The Cell.HeaderLink pattern

Every cell should have a Cell.HeaderLink. The value is either:

  • A static string: "Production Rate"
  • A bound value: "@Tag.CurrentAsset/Name"
  • A composite: "Reactor R-101: {@Tag.R101/Status}"

The header renders as a small title strip at the top of the card, in the theme's card-header style. Leaving it empty gives you a headerless card — useful for the header / status row banding the top of the display, but unusual elsewhere.

Section 4 — The TrendChart recipe

Most dashboard cells end up being trend charts. The canonical setup:

Code Block
languagejson
{
  "Type": "TrendChart",
  "Duration": "5m",
  "YMinValue": 0, "YMaxValue": 100,
  "YLabels": 5,
  "XGridLines": 6,
  "YGridLines": 5,
  "LegendPlacement": "BottomPanel",
  "VerticalCursor": true,
  "BackgroundTheme": "ControlBackground",
  "ForegroundTheme": "TextForeground",
  "BorderBrushTheme": "DefaultBorder",
  "Pens": [
    { "Type": "TrendPen", "LinkedValue": "@Tag.Reactor/Temperature_C", "PenLabel": "Temperature", "Stroke": "#FFEF4444", "StrokeThickness": 2 },
    { "Type": "TrendPen", "LinkedValue": "@Tag.Reactor/Setpoint",      "PenLabel": "Setpoint",    "Stroke": "#FF34D399", "StrokeThickness": 1 }
  ]
}

Pens accepts BOTH forms

  • Flat array: "Pens": [ {...}, {...} ] ← prefer this for readability
  • Wrapper object: "Pens": { "Type": "TrendPenList", "Children": [ {...} ] } ← older form, also accepted

TrendPen properties

Property

Required

Notes

LinkedValue

?

Tag binding: @Tag.Reactor/Temperature_C

PenLabel

Legend text

Stroke

Line color hex. Not theme-aware — hex only.

StrokeThickness

Default 1; use 2 for emphasis, 3 for setpoint/limit

YMin / YMax

Per-pen Y scale (overrides chart scale)

YAxisPosition

"Left" (default) or "Right" for dual-axis plots

Auto

true for auto-scaling that pen

Visible

true/false

Duration defaults

Duration

When

"1m"

Demos — populates visibly in ~60s

"5m"

Default operator view — recent activity

"15m"

Short-term process monitoring

"1h" / "4h"

Shift-length trends

"24h" / "7d"

Production reports, historical review

LegendPlacement

"BottomPanel" (default), "RightPanel", "TopPanel", "None". For narrow cells use "RightPanel". For wide cells "BottomPanel".

Known issue — XYChart is a schema alias (TDEV-1276)

list_elements('XYChart') currently returns the TrendChart schema byte-for-byte. For a true X-vs-Y scatter plot, this isn't it. Use TrendChart for time-series; for correlation plots wait on TDEV-1276 or compose one from Canvas primitives.

Section 5 — The DataGrid list→detail pattern

The pattern in one sentence

Use a UserType-typed Client tag as the "selected row" carrier. The DataGrid pushes selected-row values into its fields, detail controls read from its fields via @Tag.Selected<X>.Field. Zero code-behind.

The setup

  1. Create a Client-scope tag typed by your row's UDT. For a reactor list: @Tag.SelectedReactor with UserType: "ReactorRow".
  2. In the DataGrid, set SelectedValuesLink to "@Tag.SelectedReactor". The DataGrid automatically pushes values from the currently-selected row into each matching field of the UDT.
  3. Detail controls on the same display read @Tag.SelectedReactor.Name, @Tag.SelectedReactor.Temperature, etc. They update automatically as the operator clicks rows.

DataGrid definition

Code Block
languagejson
{
  "Type": "DataGrid",
  "ItemsSource": "@Dataset.Query.ActiveReactors",
  "SelectedValuesLink": "@Tag.SelectedReactor",
  "Columns": {
    "Type": "GridColumnList",
    "Children": [
      { "Type": "GridColumn", "Title": "Reactor",     "FieldName": "Name",         "Width": 120 },
      { "Type": "GridColumn", "Title": "Temp (°C)",   "FieldName": "Temperature",  "Width": 100 },
      { "Type": "GridColumn", "Title": "Pressure",    "FieldName": "Pressure",     "Width": 100 },
      { "Type": "GridColumn", "Title": "Status",      "FieldName": "Status",       "Width": 100 }
    ]
  },
  "BackgroundTheme": "ControlBackground",
  "ForegroundTheme": "TextForeground",
  "BorderBrushTheme": "DefaultBorder"
}

The detail cells (sibling cells on the same display)

...

languagejson

...

", "LinkedValue": "@Tag.

...

Reactor/Heater/Running",
      "

...

ChangeColorItems":

...

 [
        { "

...

Type": 

...

"ChangeColorItem", "

...

ChangeLimit": 

...

0, "

...

LimitColor": "

...

#FF7F1D1D" },
  

...

      { "Type": "

...

ChangeColorItem", "

...

ChangeLimit": 

...

1, "

...

LimitColor": "

...

#FFEF4444" }
      ] }
  ] },

...


{ "

...

Type": 

...

"Rectangle", "

...

Left": 

...

418, "

...

Top": 

...

280, "

...

Width": 

...

22,

...

 "

...

Height": 

...

180, "

...

FillTheme": "

...

OffFill",
  "

...

Dynamics": 

...

[ /* same FillColorDynamic */ ] }

Elements top to bottom: drive cap (Ellipse), motor housing (Rectangle), vessel body (Cylinder with ElementBlue), left jacket rail (Rectangle with heater-gated FillColorDynamic), right jacket rail (same).

For an impeller shaft: add a thin vertical Rectangle, apply RotateDynamic gated by Running:

{ "Type": "Rectangle", "Left": 344, "Top": 230, "Width": 8, "Height": 240, "FillTheme": "DefaultStroke",
  "Dynamics": [
    { 

When the operator clicks a row in the DataGrid, all three detail cells update simultaneously. No script, no event handler.

Requirements

  • The UDT's member names must match the DataGrid column FieldNames (case-sensitive).
  • The Client tag must be typed by that UDT.
  • The dataset query must return rows whose column names match the UDT members.

Section 6 — ComboBox zero-script FK-lookup

...

languagejson

...

"Type": "

...

RotateDynamic",

...

 "

...

LinkedValue": "

...

30",

...

 "IsRpm": true, "

...

OnOffLink": "

...

@Tag.Reactor/Agitator/Running" }
  

...

] }

LinkedValue: "30" and IsRpm: true means "rotate at 30 rpm." OnOffLink gates the rotation — the shaft only spins when Running=1.

Recipe 2 — Centrifugal pump (Wizard symbol with live dynamics)

{
  "

...

Type": "

...

Symbol",
  "

...

SymbolName": "

...

Wizard/

...

PUMP",
  "

...

Left": 500, "Top"

...

:

...

 300,
  "

...

Width": 80, "Height"

...

:

...

 80,
  "

...

SymbolLabels": 

...

[
    { "Type": "SymbolLabel", "Key": "State", "LabelName": "State", "LabelValue": "@Tag.Pump1/Running",  "FieldType": "Expression" },
    { "Type": "SymbolLabel", "Key": "RPM",   "LabelName": "RPM",   "LabelValue": "@Tag.Pump1/Speed",    "FieldType": "Expression" }
  ]
}

The symbol itself knows how to change its fill based on State — no extra dynamics needed.

To add a click-to-open-detail action, add the dynamic directly on the Symbol:

"Dynamics": [
  {

When the operator picks "Maria Costa" from the dropdown, the OperatorID (say 42) lands in @Tag.Shift/CurrentOperatorId automatically. Other displays binding to that tag update immediately.

Use this for: operator selection, product changeover, reactor mode select, batch selection — anywhere the user picks one row from a database-backed list.

ItemsSourceType options

  • "DataTable" — the pattern above, dataset query as source
  • "Array" — bound to an array tag
  • "StringTag" — comma-separated string (legacy, avoid)
  • "Text" — hardcoded comma-separated inline options

Section 7 — AlarmViewer (the default template is your friend)

The AlarmViewer default template ships pre-wired to @Client.AlarmPage.* context tags:

Code Block
languagejson
{
  "Type": "AlarmViewer",
  "ShowRowSelectorPane": false,
  "BackgroundTheme": "ControlBackground",
  "ForegroundTheme": "TextForeground"
}

That's the entire cell content. All alarms, all columns, all features — wired.

Custom filtering

...

languagejson

...

 "Type": "

...

ActionDynamic",
    "

...

MouseLeftButtonDown": { "Type"

...

: "DynamicActionInfo",

...

 "

...

ActionType": 

...

"OpenDisplay", "

...

ObjectLink": "PumpDetail" } }
]

Recipe 3 — Pipe segment with flow direction

{ 

Custom columns

...

languagejson

...

"Type": "

...

Gridline",

...

 

...

"

...

Left": 

...

200, "Top": 400, "Width": 

...

300, "

...

Height": 

...

60,
  "

...

Points": "

...

0,

...

30 100,30 100,0 200,0 200,60 300,60",
  "StrokeTheme": "

...

Water", "

...

StrokeThickness":

...

 6,
  "

...

StrokeLineJoin": "

...

Round",

...

 

...

"

...

StrokeLineCap": 

...

"Round" },

...


{ "Type": "

...

Polygon", "

...

Left": 490, "

...

Top": 53, 

...

"

...

Width": 

...

20, "

...

Height": 

...

14,
  

...

"

...

Points": "

...

0,0 20,7 0,14", "

...

Stretch": "

...

None",

...


  "

...

FillTheme": "

...

Water",
  "Dynamics": [
    { 

...

"

...

Available field names: AckStatus, ActiveTime_Ticks, TagName, Group, Value, ID, ItemName, State, AckRequired, Condition, SolutionName, Area, Priority, NormTime_Ticks, AckTime_Ticks, UserName, Message, Duration, Category, DateCreated_Ticks, AuxValue, AlarmLimit, PreviousValue, AuxValue2, AuxValue3. Tick fields format as DateTime at render time.

Section 8 — AssetsTree + ChildDisplay navigation

The canonical asset-driven master-detail pattern. One "detail template" display shows whatever asset the operator picks in the tree.

The tree (drop it in, no config)

...

languagejson
Type": "VisibilityDynamic", "LinkedValue": "@Tag.Pipe1/FlowRate" }
  ] }

The arrow only appears when FlowRate is non-zero — a simple, strong visual cue.

For multi-colored pipe based on flow threshold:

"Dynamics": [
  { "Type": "

...

LineColorDynamic"

...

, "LinkedValue": "@Tag.Pipe1/FlowRate",
    "ChangeColorItems": [
      { "Type": "ChangeColorItem", "ChangeLimit": 0,   "LimitColor": "#FF64748B" },
      { "Type": "ChangeColorItem", "ChangeLimit": 10,  "LimitColor": "#FF38BDF8" },
      { "Type": "ChangeColorItem", "ChangeLimit": 50,  "LimitColor": "#FF0369A1" }
    ] }
]

Recipe 4 — Reactor with heating coils

Uses Recipe 1 (vessel) + Path overlay for coils:

{ /* Recipe 1 elements for vessel body with jacket */ },

The default template already binds:

  • LinkedValue@Client.Context.AssetName (output — which asset is selected)
  • AssetPathLink@Client.Context.AssetPath (output — full path)

The detail panel — ChildDisplay

Code Block
languagejson
{
  "Type": "ChildDisplay",
  "DisplayLink": "EquipmentDetailTemplate"
}

Inside EquipmentDetailTemplate, every binding uses Asset(@Client.Context.AssetPath + ".Property") or direct @Tag paths constructed from the context. When the operator clicks a different tree node, the ChildDisplay re-renders with the new asset's data.

Dynamic ChildDisplay

Code Block
languagejson
{
  "Type": "ChildDisplay",
  "DisplayLink": "@Tag.DetailDisplayName"
}

A Script calculates which detail display to show based on the selected asset's type (Pump → PumpDetail, Reactor → ReactorDetail) and writes to @Tag.DetailDisplayName. The ChildDisplay swaps automatically.

Section 9 — Carousel and TabControl

Both accept the same structure: HeaderElements (navigation chrome) and TabItems (panels). Difference:

  • CarouselAutoCycleLink: 5 for 5-second auto-cycling. Lobby displays, rotating KPI boards.
  • TabControl — no auto-cycle; operator clicks tabs. Drill-down detail panels.

...

languagejson

...

{ "Type": "

...

Path", "

...

Left": 305, "

...

Top": 

...

220,

...

 "Width": 130, "

...

Height": 

...

240,
 

...

 "

...

Data": "

...

M0,

...

0 Q65,20 130,0 M0,40 Q65,60 130,40 M0,80 Q65,100 130,80 M0,120 Q65,140 130,120 M0,160 Q65,180 130,160 M0,200 Q65,220 130,200",
  "StrokeTheme": "StateRed",
  "StrokeThickness": 2,
  "Fill": "#00000000", "FillTheme": "",
  "Dynamics": [
    { "Type": "

...

VisibilityDynamic", "LinkedValue": "

...

@Tag.Reactor/Heater/Running" }

...

Rules:

  • Only ONE TabItem should have IsSelected: true
  • Children is an array — can contain multiple elements per tab
  • AutoCycleLink can be a number (seconds) OR a tag binding ("@Tag.ShowroomCycleSeconds") for runtime-configurable cycling

Section 10 — Common KPI card recipes

Big-number CenterValue

Code Block
languagejson
{
  "Type": "CenterValue",
  "LinkedValue": "@Tag.Plant/ProductionRate",
  "CenterTextFormat": "N1",
  "AccentTextLink": "units/hr",
  "CenterFontSize": 48,
  "AccentFontSize": 14,
  "BackgroundTheme": "ControlBackground"
}

CenterTextFormat: "N0" (integer), "N1" (1 decimal), "N2" (2 decimals), "P1" (percent 1 decimal). AccentTextLink is the unit or caption under/beside the main number.

Composite TextBlock (value + unit + context in one)

Code Block
languagejson
{
  "Type": "TextBlock",
  "LinkedValue": "Throughput: {@Tag.Plant/ProductionRate} units/hr ({@Tag.Plant/TargetPct}% of target)",
  "FontSize": 16,
  "FontWeight": "SemiBold",
  "ForegroundTheme": "TextForeground"
}

Threshold-colored value

...

languagejson

  ] }

The coils are only visible when the heater is active — a subtle but unmistakable visual cue for operators.

Section 6 — Dynamics reference

Call list_dynamics() for the full list. Here are the 14 types grouped by what they do:

Action category

DynamicWhat it doesUse for
ActionDynamicRun an action on mouse eventNavigation, tag writes, scripts, toggles
CodeBehindDynamicRun code on display lifecycle eventsInit, cleanup, periodic updates
HyperlinkDynamicOpen external URL on clickDocumentation links, external dashboards

Color category

DynamicWhat it doesUse for
FillColorDynamicChange element Fill based on thresholdsStatus indicators, process meaning
LineColorDynamicChange element Stroke based on thresholdsPipe color-by-flow, boundary-alarm borders
TextColorDynamicChange text Foreground based on thresholdsAlert text, highlighted values

Animation category (Canvas-only)

DynamicWhat it doesUse for
RotateDynamicRotate element by angle or rpmPointers, impeller shafts, motors, compass needles
ScaleDynamicScale element by tag valueGrowth/shrink animations, level indicators
MoveDragDynamicMove element by tag value OR let user dragSliding valves, operator drag-to-set
SkewDynamicSkew elementPerspective effects, rare

Data category (Canvas-only)

DynamicWhat it doesUse for
BargraphDynamicFill a Rectangle proportionally based on valueTank level fill, progress fills, power meter strips

Visibility, Security, Feedback

DynamicWhat it doesUse for
VisibilityDynamicShow/hide based on expressionConditional panels, state-dependent overlays, coils-only-when-heating
SecurityDynamicHide/disable based on user permissionAdmin-only controls, role-based HMI
ShineDynamicMouse-over highlight / glow effectHover feedback on clickable shapes

Copy-paste patterns

status_indicator (shape that changes color on running/stopped):

{
  "Type": "Ellipse",
  "Left": 100, "Top": 100, "Width": 24, "Height": 24,
  "Dynamics": [
    

...

{ "Type": "

...

FillColorDynamic", "

...

LinkedValue":

...

 "

...

@Tag.Equipment/Running",
  

...

    "ChangeColorItems": [
        { "Type": "ChangeColorItem", "ChangeLimit": 

...

0, "LimitColor": "

...

#FF808080" },
        { "Type": "ChangeColorItem", "ChangeLimit": 

...

1, "LimitColor": "

...

#FF34D399" }
      ] }
  

...

Blue cold → green normal → amber warning → red alarm.

Section 11 — Navigation on Dashboard displays

Same rule as Canvas: page-to-page navigation belongs in the Header display, not on content pages. Content pages get only in-page interactions.

Tab-bar header pattern

...

]
}

toggle_button (click toggles a boolean, color reflects state):

...

{
  "

...

Type": "

...

Rectangle",
  "Left": 100, "

...

Top": 

...

200, "Width": 120, "Height": 40,
  "

...

Dynamics": [
    { "Type": "ActionDynamic",
      "

...

MouseLeftButtonDown": 

...

{ "

...

Type"

...

: "

...

DynamicActionInfo", "

...

ActionType"

...

: "

...

ToggleValue", "

...

ObjectLink"

...

: "@Tag.Motor/Start" } },
    { "Type": "FillColorDynamic", "

...

LinkedValue": 

...

"

...

@Tag.Motor/Start"

...

,
      "

...

ChangeColorItems": [
        { "

...

Type": 

...

"ChangeColorItem", "

...

ChangeLimit": 0, "

...

LimitColor": "#FFEF4444" },
        { "Type": "

...

ChangeColorItem", "ChangeLimit": 1, "

...

LimitColor": "

...

#FF34D399"

...

 }
      ] },
   

...

 

...

{ "Type": "

...

ShineDynamic" }
  ]
}

animated_motor (continuous rotation gated by boolean):

{
  "Type": "

...

Symbol",
  "

...

SymbolName": "

...

Wizard/MOTOR",

...


 

...

 "

...

Left": 

...

400, "

...

Top": 

...

300, "

...

Width": 

...

80, "

...

Height": 

...

80,
  

...

"Dynamics": [
    { "

...

Type": 

...

"RotateDynamic", "

...

LinkedValue": 

...

"30", "

...

IsRpm": 

...

true, "

...

OnOffLink": "

...

@Tag.Motor/Running" }
  ]
}

level_bar (tank-fill effect):

{
  "Type": "

...

Rectangle",
  

...

"Left": 200, "Top": 200, "Width": 120, "Height": 240,
  "Dynamics": [
    { "Type": "

...

BargraphDynamic", "

...

LinkedValue": "

...

@Tag.Tank/Level",

...


      

...

"

...

MinValueLink": "0", "

...

MaxValueLink": 

...

"100",
      "BarColor": "

...

#FF38BDF8",
      "

...

Orientation": "

...

VerticalUp" }

...

  

...

]

...

}

ActionDynamic action types

ActionTypeExtra fieldsWhat it does
OpenDisplayObjectLink: "DisplayName"Navigate to another display
ToggleValueObjectLink: "@Tag.X"Flip a boolean tag
SetValueObjectLink: "@Tag.X", ObjectValueLink: valueWrite a specific value
RunScriptActionScript: "MethodName"Run a CodeBehind method

Mouse events beyond MouseLeftButtonDown: MouseRightButtonDown, MouseDoubleClick, MouseEnter, MouseLeave. Each is a top-level key on the ActionDynamic.

Format rules

  • Dynamics go in the Dynamics array. Never as direct properties on the element.
  • ChangeColorItems is a flat array. Not {"Type": "ColorChangeList", "Children": [...]}.
  • An element can have MULTIPLE dynamics of different types in the same Dynamics array.

Page-to-page navigation belongs in the Header display, not on content pages. Content pages only get in-page actions (start/stop, open popup, acknowledge).

Workflow

  1. Build all your content pages first.
  2. Read the Startup layout: get_objects('DisplaysLayouts', names=['Startup'], detail='full').
  3. Read the Header display: get_objects('DisplaysList', names=['Header'], detail='full').
  4. Add navigation buttons right-aligned in the header (100–130 × 30–35, 10px gap).
  5. Write the modified Header display back.

Back-navigation on detail pages

{
  "Type": "TextBlock",
  "Left": 48, "Top": 32, "Width": 400, "Height": 20,
  "LinkedValue": "← Process Area Overview",
  "ForegroundTheme": "AccentBrush",
  "Dynamics": [
    { "Type": "ActionDynamic",
      "MouseLeftButtonDown": { "Type": "DynamicActionInfo", "ActionType": "OpenDisplay", "ObjectLink": "ProcessAreaOverview" } }
  ]
}

Section 8 — Canvas quirks and write-time normalizations

  • Cylinder / Gear / Arrow / Cloud / Star / Hexagon / Pentagon / Trapezoid become Path or Polygon on disk. Auto-geometry expanded.
  • SvgGroup → ShapeGroup on save. SVG parsed into native WPF shapes.
  • Theme brushes stored with theme: prefix: {"FillTheme": "ElementBlue"}{"Fill": "theme:ElementBlue"}.
  • Stretch: "Fill" default silently dropped on Polygon/Polyline/Path/Spline. If you want Stretch: "None", it IS stored.
  • Background default #FFFAFAFA baked in. Explicitly set Background: "theme:PageBackground".
  • Polygon without Points silently skipped. Always include Points.

Section 9 — Canvas checklist

  • ? Background set explicitly (not #FFFAFAFA default)
  • ? Zones calculated to fill the full canvas width and height
  • ? Every zone has a background Rectangle with FillTheme: "PanelBackground" — FIRST in Elements
  • ? Symbols ≥ 60×60 (Wizard), ≥ 80×80 recommended on process overview displays
  • ? Library symbols scaled proportionally — maintain aspect ratio
  • ? No equipment symbols overlap unless intentionally layered
  • ? Zone titles FontSize ≥ 14, value text FontSize ≥ 12
  • ? Value and Unit in the SAME TextBlock with composite LinkedValue
  • ? No element extends beyond the display's Width/Height
  • ? Page navigation is in the Header, not on content pages
  • ? All dynamics inside Dynamics array (never as direct properties)
  • ? ChangeColorItems is a flat array (no ColorChangeList wrapper)
  • ? No use of @Label. in element bindings (only @Tag.)
  • ? All shape / symbol types verified via list_elements() before use
  • ? get_state after write shows errorList empty

Section 10 — Quick reference

The 10 element types you'll actually use on most Canvas displays

Rectangle         - backgrounds, bars, pipes (when rectangular)
Ellipse           - caps, LEDs, pump bodies
Polygon    

Minimum button size: 100×32. Recommended: 130×40 for touch-friendly control rooms.

Row-click drill-down

For DataGrids, a double-click to drill into detail:

Code Block
languagejson
"Dynamics": [
  { "Type": "ActionDynamic",
    "MouseDoubleClick": { "Type": "DynamicActionInfo", "ActionType": "OpenDisplay", "ObjectLink": "ReactorDetail" } }
]

The target display reads @Tag.SelectedReactor.Name (etc.) — so you get "double-click row → open detail view of that row" with zero code.

Section 12 — Dashboard-specific quirks

  1. Cells size by their content, NOT the other way around. If you set a Row: "*" to expand, but the cell content is a fixed-size gauge, the gauge doesn't stretch — the cell is as tall as the row, but the gauge stays its native size centered in the cell. For content that should fill the cell, use controls with stretch-friendly behavior: TrendChart, BarChart, AlarmViewer, DataGrid. Gauges are a fixed size.
  2. Dynamics that don't work in Dashboard. Silently ignored: RotateDynamic, ScaleDynamic, MoveDragDynamic, SkewDynamic, BargraphDynamic. If you need a rotating element or a custom level-bar, move that cell's content to a Canvas display embedded via ChildDisplay, or pick a control with the behavior built in (LinearGauge for level, symbols for motor-running-spin).
  3. Cell headers use theme styles you can't directly override. Cell.HeaderLink renders in a theme-defined style — you can't set FontSize or Foreground on the header itself from the cell. If you need a custom header, set Cell.HeaderLink to empty string and put a TextBlock as the first element inside the cell Content.
  4. ComboBox + DataTable source loads on first render. Expect a brief empty state on display open. For critical selection flows, bind to @Tag.SelectedValueLink with a pre-populated default.
  5. ChildDisplay depth limit. Don't nest ChildDisplays more than 2 levels deep. Beyond that, performance degrades and context-tag reuse becomes confusing.

Section 13 — Dashboard checklist

  • ? PanelType: "Dashboard" is set
  • ? DashboardDisplay object includes Columns, Rows, Cells
  • ? Every cell has Row, Col, and either Cell.HeaderLink (with content) or omitted for header-less cells
  • ? No Canvas-only element types (Rectangle, Ellipse, Polygon, Cylinder, ShapeGroup, SvgGroup, Group) in Content
  • ? No Canvas-only dynamics (Rotate, Scale, MoveDrag, Skew, Bargraph) on cell content
  • ? Text values ≥ 14 FontSize; big KPIs ≥ 22 FontSize
  • ? ColSpan/RowSpan summed values don't exceed the grid
  • ? AlarmViewer uses default template (no Columns override) unless you NEED custom columns
  • ? DataGrid SelectedValuesLink ↔ UserType-typed Client tag ↔ detail cells binding chain verified
  • ? ComboBox with DataTable source has DisplayMember AND SelectedValuePath AND SelectedValueLink
  • ? TrendChart Pens are flat array form; each pen has at minimum LinkedValue + Stroke
  • ? Header display owns all page-to-page navigation
  • ? get_state after write shows errorList empty

Section 14 — Quick reference

The 10 controls you'll actually use on most Dashboards

...

       - 

...

arrows, 

...

Canonical dashboard envelope

...

languagejson

...

funnels, custom closed shapes
Gridline          - pipes (always prefer over Polyline)
Path              - curves, coils, complex shapes
Cylinder      

...

 

...

 

...

 

...

 - vessels, tanks (first-class shortcut)
TextBlock      

...

 

...

 

...

 - all text (labels, values, composite)
Symbol      

...

 

...

 

...

 

...

 

...

 

...

 - Wizard/Library/Solution symbols
ShapeGroup      

...

 

...

 - composed equipment with unified dynamics
RadialGauge      

...

 

...

- temperature, pressure, any circular gauge

Tool-call recipes

# Before first use of an element
list_elements('RadialGauge')

# Before first use of a dynamic
list_dynamics('FillColorDynamic')

# Pattern search in library
list_elements('Library/HMI/Pumps')

# Wizard catalog (always 5 symbols)
list_elements('Wizard')

# Theme brushes
list_elements('ThemeColors')