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:
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:
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.
...
The whole display is defined by:
Columns and Rows arrays describing the grid track sizesRow, Col, optional RowSpan/ColSpan, a Cell.HeaderLink for the card title, and Content (the element that fills the cell)| Code Block | ||
|---|---|---|
| ||
{
"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 */ } }
]
}
} |
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 pixelsStandard patterns:
Layout | Columns | Rows |
|---|---|---|
3 equal columns, 2 rows |
|
|
Header + 2 equal rows |
|
|
Sidebar + main |
|
|
Header + sidebar + main |
|
|
4 KPIs across top, trend below |
|
|
Operator wall (4×3) |
|
|
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.
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.
Six data cards in a 3-column × 2-row grid:
...
| language | json |
|---|
...
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.
| Canvas | Width × Height | When |
|---|---|---|
| Standard HD | 1366 × 728 | Default, works everywhere |
| Wide | 1600 × 900 | Modern control-room monitors |
| Full HD | 1920 × 1080 | Dedicated operator displays |
| 4K scaled | 3840 × 2160 | Rare — prefer 1920×1080 with StretchFill |
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 = zoneHeightFor a 1600×900 canvas with 3 zones: each zone is ~515 wide × 640 tall. For 4 zones: ~381 wide. For 2 zones: ~780 wide.
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:
All Canvas-only. All theme-aware (Fill/FillTheme, Stroke/StrokeTheme, StrokeThickness).
| Shape | Required | Use when |
|---|---|---|
| Rectangle | — | Panels, status bars, tank bodies, bars, backgrounds. Has RadiusX/RadiusY for rounded corners. |
| Ellipse | — | Tank caps (top/bottom), status LEDs, sight glasses, pump bodies. Circle when Width=Height. |
| Polygon | Points ≥3 | Arrows, funnels, hoppers, diamonds. Auto-closes. Set Stretch: "None" to preserve exact point coords. |
| Polyline | Points ≥2 | Open curved lines. Doesn't close. Prefer Gridline for pipes. |
| Path | Data | Any curve, arc, complex shape. SVG mini-language (M L H V C Q A Z). |
| Gridline | Points ≥2 | Use for pipes and orthogonal connections. Constrained to horizontal/vertical segments. The P&ID convention. |
| Spline | Points ≥2 | Smooth curves through control points (Catmull-Rom). Rare — Path covers most cases. |
{
"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"
}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.
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.
| Type | Default geometry | Use for |
|---|---|---|
| Cylinder | Vertical cylinder with elliptical caps | Tanks, vessels, drums, silos |
| Gear | 8-tooth gear | Machinery icons, manual/auto toggles |
| Arrow | Right-pointing arrow | Flow direction, callouts (rotate via RotateDynamic for other directions) |
| Cloud | Puffy cloud outline | MQTT broker, cloud service, weather |
| Star | 5-pointed star | Favorites, highlights, quality marker |
| Hexagon | Regular hexagon | Node diagrams, honeycomb layouts |
| Pentagon | Regular pentagon | Rare — use for ANSI warning signs |
| Trapezoid | Isosceles trapezoid | Hoppers, 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.
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.
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.
| Container | Children | Dynamics apply to | Use for |
|---|---|---|---|
| ShapeGroup | Shapes only | ALL children uniformly | "The whole vessel turns alarm red when Running=0" |
| SvgGroup | Auto-parsed from inline SVG string | ALL children (normalized to ShapeGroup on write) | "I have an SVG, I want dynamics per element" |
| Group | Any element type | Each child has independent dynamics | "Interactive panel with chart+buttons that moves as a unit" |
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",
...
...
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" }
]
}
]
}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.
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 }}] }
]
}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": "FillColorDynamicAsset tree on the left, detail panel on the right:
| Code Block | ||
|---|---|---|
| ||
{
"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."
| Code Block | ||
|---|---|---|
| ||
{ "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.
Cell purpose | Best control | Why |
|---|---|---|
Display title / section header |
|
|
Single-value KPI (numeric) |
| Pre-styled big-number-plus-unit tile |
Single-value KPI (string) |
| Simpler than CenterValue for strings |
Time-series trend |
| The workhorse |
Tag value dial |
| Gauge with threshold bands |
Tag value bar (linear) |
| For bar-style KPIs with setpoint pointer |
Cumulative count / meter |
| Specialty numeric readouts |
Categorical comparison |
| e.g., output by shift |
Current alarms |
| Pre-wired to Client.AlarmPage context |
Asset hierarchy navigation |
| Pre-wired to Client.Context |
Dropdown selector |
| Zero-script FK lookup |
Data table / list |
| The list in list→detail |
Document viewer (SOP, work order) |
| URL-bound inline PDF |
Fleet/plant map |
| Lat/long-positioned markers |
Embedded another display |
| Reusable detail panels |
Multiple panels user switches |
| Manual click-to-switch |
Slideshow of status boards |
| Auto-cycle with AutoCycleLink |
Collapsible panel |
| Click to expand advanced settings |
Card-per-item layout |
| N items from a data source, one template |
Cell.HeaderLink patternEvery cell should have a Cell.HeaderLink. The value is either:
"Production Rate""@Tag.CurrentAsset/Name""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.
Most dashboard cells end up being trend charts. The canonical setup:
| Code Block | ||
|---|---|---|
| ||
{
"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": [ {...}, {...} ] ← prefer this for readability"Pens": { "Type": "TrendPenList", "Children": [ {...} ] } ← older form, also acceptedProperty | Required | Notes |
|---|---|---|
| ? | Tag binding: |
| — | Legend text |
| — | Line color hex. Not theme-aware — hex only. |
| — | Default 1; use 2 for emphasis, 3 for setpoint/limit |
| — | Per-pen Y scale (overrides chart scale) |
| — |
|
| — |
|
| — |
|
Duration | When |
|---|---|
| Demos — populates visibly in ~60s |
| Default operator view — recent activity |
| Short-term process monitoring |
| Shift-length trends |
| Production reports, historical review |
"BottomPanel" (default), "RightPanel", "TopPanel", "None". For narrow cells use "RightPanel". For wide cells "BottomPanel".
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.
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.
@Tag.SelectedReactor with UserType: "ReactorRow".SelectedValuesLink to "@Tag.SelectedReactor". The DataGrid automatically pushes values from the currently-selected row into each matching field of the UDT.@Tag.SelectedReactor.Name, @Tag.SelectedReactor.Temperature, etc. They update automatically as the operator clicks rows.| Code Block | ||
|---|---|---|
| ||
{
"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"
} |
...
| language | json |
|---|
...
", "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.
FieldNames (case-sensitive)....
| language | json |
|---|
...
"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.
{
"...
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.
"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 optionsThe AlarmViewer default template ships pre-wired to @Client.AlarmPage.* context tags:
| Code Block | ||
|---|---|---|
| ||
{
"Type": "AlarmViewer",
"ShowRowSelectorPane": false,
"BackgroundTheme": "ControlBackground",
"ForegroundTheme": "TextForeground"
} |
That's the entire cell content. All alarms, all columns, all features — wired.
...
| language | json |
|---|
...
"Type": "...
ActionDynamic",
"...
MouseLeftButtonDown": { "Type"...
: "DynamicActionInfo",...
"...
ActionType": ...
"OpenDisplay", "...
ObjectLink": "PumpDetail" } }
]{ ...
| language | json |
|---|
...
"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.
The canonical asset-driven master-detail pattern. One "detail template" display shows whatever asset the operator picks in the tree.
...
| language | json |
|---|
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" }
] }
]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)| Code Block | ||
|---|---|---|
| ||
{
"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.
| Code Block | ||
|---|---|---|
| ||
{
"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.
Both accept the same structure: HeaderElements (navigation chrome) and TabItems (panels). Difference:
AutoCycleLink: 5 for 5-second auto-cycling. Lobby displays, rotating KPI boards....
| language | json |
|---|
...
{ "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:
IsSelected: trueChildren is an array — can contain multiple elements per tabAutoCycleLink can be a number (seconds) OR a tag binding ("@Tag.ShowroomCycleSeconds") for runtime-configurable cycling| Code Block | ||
|---|---|---|
| ||
{
"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.
| Code Block | ||
|---|---|---|
| ||
{
"Type": "TextBlock",
"LinkedValue": "Throughput: {@Tag.Plant/ProductionRate} units/hr ({@Tag.Plant/TargetPct}% of target)",
"FontSize": 16,
"FontWeight": "SemiBold",
"ForegroundTheme": "TextForeground"
} |
...
| language | json |
|---|
] }The coils are only visible when the heater is active — a subtle but unmistakable visual cue for operators.
Call list_dynamics() for the full list. Here are the 14 types grouped by what they do:
| Dynamic | What it does | Use for |
|---|---|---|
ActionDynamic | Run an action on mouse event | Navigation, tag writes, scripts, toggles |
CodeBehindDynamic | Run code on display lifecycle events | Init, cleanup, periodic updates |
HyperlinkDynamic | Open external URL on click | Documentation links, external dashboards |
| Dynamic | What it does | Use for |
|---|---|---|
FillColorDynamic | Change element Fill based on thresholds | Status indicators, process meaning |
LineColorDynamic | Change element Stroke based on thresholds | Pipe color-by-flow, boundary-alarm borders |
TextColorDynamic | Change text Foreground based on thresholds | Alert text, highlighted values |
| Dynamic | What it does | Use for |
|---|---|---|
RotateDynamic | Rotate element by angle or rpm | Pointers, impeller shafts, motors, compass needles |
ScaleDynamic | Scale element by tag value | Growth/shrink animations, level indicators |
MoveDragDynamic | Move element by tag value OR let user drag | Sliding valves, operator drag-to-set |
SkewDynamic | Skew element | Perspective effects, rare |
| Dynamic | What it does | Use for |
|---|---|---|
BargraphDynamic | Fill a Rectangle proportionally based on value | Tank level fill, progress fills, power meter strips |
| Dynamic | What it does | Use for |
|---|---|---|
VisibilityDynamic | Show/hide based on expression | Conditional panels, state-dependent overlays, coils-only-when-heating |
SecurityDynamic | Hide/disable based on user permission | Admin-only controls, role-based HMI |
ShineDynamic | Mouse-over highlight / glow effect | Hover feedback on clickable shapes |
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.
Same rule as Canvas: page-to-page navigation belongs in the Header display, not on content pages. Content pages get only in-page interactions.
...
]
}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" }
...
...
]
...
}| ActionType | Extra fields | What it does |
|---|---|---|
OpenDisplay | ObjectLink: "DisplayName" | Navigate to another display |
ToggleValue | ObjectLink: "@Tag.X" | Flip a boolean tag |
SetValue | ObjectLink: "@Tag.X", ObjectValueLink: value | Write a specific value |
RunScript | ActionScript: "MethodName" | Run a CodeBehind method |
Mouse events beyond MouseLeftButtonDown: MouseRightButtonDown, MouseDoubleClick, MouseEnter, MouseLeave. Each is a top-level key on the ActionDynamic.
Dynamics array. Never as direct properties on the element.ChangeColorItems is a flat array. Not {"Type": "ColorChangeList", "Children": [...]}.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).
get_objects('DisplaysLayouts', names=['Startup'], detail='full').get_objects('DisplaysList', names=['Header'], detail='full').{
"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" } }
]
}Path or Polygon on disk. Auto-geometry expanded.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.#FFFAFAFA baked in. Explicitly set Background: "theme:PageBackground".Points.Background set explicitly (not #FFFAFAFA default)FillTheme: "PanelBackground" — FIRST in ElementsDynamics array (never as direct properties)ChangeColorItems is a flat array (no ColorChangeList wrapper)@Label. in element bindings (only @Tag.)list_elements() before useget_state after write shows errorList emptyRectangle - backgrounds, bars, pipes (when rectangular)
Ellipse - caps, LEDs, pump bodies
Polygon Minimum button size: 100×32. Recommended: 130×40 for touch-friendly control rooms.
For DataGrids, a double-click to drill into detail:
| Code Block | ||
|---|---|---|
| ||
"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.
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.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).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.@Tag.SelectedValueLink with a pre-populated default.PanelType: "Dashboard" is setDashboardDisplay object includes Columns, Rows, CellsRow, Col, and either Cell.HeaderLink (with content) or omitted for header-less cellsDisplayMember AND SelectedValuePath AND SelectedValueLinkLinkedValue + Strokeget_state after write shows errorList empty...
- ...
arrows, ...
...
| language | json |
|---|
...
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# 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')