Canvas displays are the paradigm for 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 (load Skill Display Construction — Dashboard instead) when the 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 gotchas, and spacing/typography tokens.
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 = 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.
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 |
Ellipse | — | Tank caps (top/bottom), status LEDs, sight glasses, pump bodies. Circle when Width=Height. |
Polygon |
| Arrows, funnels, hoppers, diamonds. Auto-closes. Set |
Polyline |
| Open curved lines. Doesn't close. Prefer Gridline for pipes. |
Path |
| Any curve, arc, complex shape. SVG mini-language ( |
Gridline |
| Use for pipes and orthogonal connections. Constrained to horizontal/vertical segments. The P&ID convention. |
Spline |
| 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() also reports Triangle, Octagon, Parallelogram, Hill, Curve, Senoid, Wave, T-Junction under the Control category. Do NOT use these. They're schema phantoms — neither Type: "X" alone nor Type: "X" + SymbolName: "Wizard/X" works. For a triangle, use Polygon with three Points. For a "T-junction" on a pipe, use two Gridlines.
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",
"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": "FillColorDynamic", "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": [
{ "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": [
{ "Type": "ActionDynamic",
"MouseLeftButtonDown": { "Type": "DynamicActionInfo", "ActionType": "OpenDisplay", "ObjectLink": "PumpDetail" } }
] |
{ "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": [
{ "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 */ },
{ "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" }
] } |
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 |
|---|---|---|
| Run an action on mouse event | Navigation, tag writes, scripts, toggles |
| Run code on display lifecycle events | Init, cleanup, periodic updates |
| Open external URL on click | Documentation links, external dashboards |
Dynamic | What it does | Use for |
|---|---|---|
| Change element Fill based on thresholds | Status indicators, process meaning |
| Change element Stroke based on thresholds | Pipe color-by-flow, boundary-alarm borders |
| Change text Foreground based on thresholds | Alert text, highlighted values |
Dynamic | What it does | Use for |
|---|---|---|
| Rotate element by angle or rpm | Pointers, impeller shafts, motors, compass needles |
| Scale element by tag value | Growth/shrink animations, level indicators |
| Move element by tag value OR let user drag | Sliding valves, operator drag-to-set |
| Skew element | Perspective effects, rare |
Dynamic | What it does | Use for |
|---|---|---|
| Fill a Rectangle proportionally based on value | Tank level fill, progress fills, power meter strips |
Dynamic | What it does | Use for |
|---|---|---|
| Show/hide based on expression | Conditional panels, state-dependent overlays, coils-only-when-heating |
| Hide/disable based on user permission | Admin-only controls, role-based HMI |
| 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" }
] }
]
} |
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 |
|---|---|---|
|
| Navigate to another display |
|
| Flip a boolean tag |
|
| Write a specific value |
|
| 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": "? Granulation Cell A",
"ForegroundTheme": "AccentBrush",
"Dynamics": [
{ "Type": "ActionDynamic",
"MouseLeftButtonDown": { "Type": "DynamicActionInfo", "ActionType": "OpenDisplay", "ObjectLink": "GranulationCellA" } }
]
} |
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.).SourceIri (TDEV-1273 workaround: literal text)get_state after write shows errorList emptyRectangle - backgrounds, bars, pipes (when rectangular) Ellipse - caps, LEDs, pump bodies Polygon - arrows, 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') |