Skip to content

6. Layer 3 — Notation (visual definition)

Contents of DISL 0.2

The notation layer defines the concrete syntax: how every node, edge, port and label looks, from a plain rectangle to a finely parameterised shape with draggable handles and embedded form widgets, and from a straight line to a rounded orthogonal route with a double casing, custom arrowheads and labels at its start, middle and end.

6.1 Overview and resolution

{
  "notation": {
    "theme":   { "tokens": {}, "modes": { "dark": {} } },
    "styles":  { "base": {}, "state": { "extends": "base" } },
    "shapes":  { "folder": {} },
    "markers": { "openDiamond": {} },
    "icons":   { "bolt": { "svg": "…" } },
    "nodes":   { "State": {} },
    "edges":   { "Transition": {} },
    "ports":   { "Block.in": {} },
    "canvas":  {},
    "doc":     "…"
  }
}

Resolution of a visual property. For a given element, the value of a visual property (for example stroke.width of a node's body) is resolved in this order; the first value found wins:

  1. Per-element style override stored in the DID definition's view data (11.6), if the specification allows overrides.
  2. Interaction state styles (states.selected, states.hover, …) that are active, in the fixed priority dragging > selected > focused > hover > highlighted > dropTarget > invalid > warning > disabled.
  3. Conditional styles (conditions) whose when expression is true, in declaration order (later wins).
  4. Variants (variants) whose when is true — they may replace shape, size, labels and style.
  5. The notation's own style (inline or referenced), with extends chains resolved.
  6. The notation of the nearest supertype (C3 order), repeating steps 2–5.
  7. The viewpoint override and then the global notation.defaults for the element kind.
  8. The built-in default (Appendix B.1).

Properties merge per key: a state style that only sets stroke.color keeps the resolved stroke.width.

What is drawn. Which elements of the model a view draws is decided in one order, and filters, budgets, legends, notices and the empty-canvas message all read its result. A runtime MUST compute the drawn elements of a view in these steps, each working on what the previous step left:

  1. Viewpoint membership: the elements whose type the viewpoint includes and does not exclude, narrowed by its members when it declares them (3.5).
  2. Derived elements: the derived nodes and relations (4.11) computed from the model are added.
  3. Viewer filters: elements hidden by viewer state, such as the children of a collapsed container and elements a canvas filter (6.13) filters out, are removed.
  4. Budgets: when a hard budget (3.2) is exceeded, the elements it cuts are removed, so a budget applies to what the filters leave.
  5. Visibility: elements whose resolved notation has visible: false are removed.

A relation MUST be drawn only when both of its ends are drawn, or as a stub where its notation declares one (6.10); a relation with an undrawn end is otherwise not drawn. The result, in model order, is the Diagram member diagram.drawn (12.2). Because steps 3 and 4 depend on the viewer, diagram.drawn MUST NOT be read in the deterministic contexts of 12.5; a constraint about what a view shows uses over: "view" and view.members instead (8.2). Undrawn elements still exist in the model, are still validated, and may still be named in messages and reasons (2.3).

6.2 Theme and tokens

Tokens give colors, fonts and sizes semantic names so that one specification renders correctly in light mode, dark mode, high contrast and the brand palette of an organisation.

{
  "theme": {
    "tokens": {
      "color.surface":      "#FFFFFF",
      "color.text":         "#1F1E1C",
      "color.border":       "#8A8880",
      "color.accent":       "#534AB7",
      "color.accent.soft":  "#EEEDFE",
      "color.danger":       "#E24B4A",
      "font.body":          "Inter, system-ui, sans-serif",
      "font.mono":          "JetBrains Mono, monospace",
      "size.corner":        8,
      "size.stroke":        1.25
    },
    "modes": {
      "dark": {
        "color.surface":     "#1F1E1C",
        "color.text":        "#F1EFE8",
        "color.border":      "#B4B2A9",
        "color.accent.soft": "#3C3489"
      },
      "high-contrast": { "color.border": "#000000", "size.stroke": 2 }
    },
    "defaultMode": "light",
    "followSystem": true
  }
}
Property Type Description
tokens map qualified id → value Base token values (colors, font stacks, numbers, strings).
modes map mode → token map Overrides per mode. Mode names are free; light, dark and high-contrast are recognised by runtimes.
defaultMode string Initial mode.
followSystem bool Follow the operating system's light/dark preference.
extends string Name of an imported theme to extend ("corp.theme").
contrast ContrastRequirement[] Pairs of tokens whose contrast ratio a validator checks (below).

Contrast requirements. A ContrastRequirement ($defs/ContrastRequirement) is { "foreground": token, "background": token or token[], "minRatio": number ≥ 1, "doc": Doc }. A specification validator MUST compute the WCAG 2.x contrast ratio of every foreground and background pair in every theme mode (the mode's tokens merged over tokens) and MUST report each pair below minRatio as a specification warning. Runtimes are unaffected. 6.15 recommends 4.5 for text.

"contrast": [ { "foreground": "color.text", "background": ["fdg.ui", "fdg.data", "fdg.action", "fdg.function"], "minRatio": 4.5 } ]

Tokens are referenced with { "token": "color.accent" } in any Bindable position, and from CEL with token('color.accent'). Runtimes MAY let users switch modes; DID definitions never store resolved token values unless a user explicitly overrides a style.

6.3 Paint

A Paint fills an area or colors a stroke.

Form Example
Color "#EEEDFE", "transparent", "currentColor"
Token / CEL / attribute { "token": "color.accent" }, { "cel": "self.done ? '#639922' : '#BA7517'" }, { "attribute": "color" }
none "none" — no paint (unlike transparent, none is not hit-testable).
Linear gradient { "type": "linear", "angle": 90, "stops": [ { "offset": 0, "color": "#FAC775" }, { "offset": 1, "color": "#EF9F27" } ] }
Radial gradient { "type": "radial", "center": [0.5, 0.5], "radius": 0.7, "stops": [ … ] }
Pattern { "type": "pattern", "pattern": "hatch", "color": "#888780", "background": "#FFFFFF", "spacing": 6, "angle": 45, "width": 1 }
Image { "type": "image", "src": "icons/brick.png", "fit": "tile", "opacity": 0.5 }

Built-in patterns: hatch, cross-hatch, dots, grid, vertical, horizontal, zigzag, checker, bricks. Patterns are also RECOMMENDED as a secondary visual cue when color distinguishes categories, for accessibility (6.15).

Gradient stops accept Bindable colors, so gradients can follow tokens. offset is a fraction.

6.4 Stroke and line styles

A Stroke describes how an outline or a line is drawn. The same object is used for node outlines, edge lines, separators, grid lines and marker outlines.

Property Type Description
color Paint Default { "token": "color.border" } or #5F5E5A.
width Bindable number Canvas units. Default 1.
minWidthScreenPx number Lines never render thinner than this on screen when zoomed out (hairline preservation).
opacity fraction Default 1.
dash DashStyle See below. Default "solid".
dashOffset number Phase of the dash pattern.
dashScale "absolute", "width" Whether dash lengths are canvas units or multiples of width. Default "width".
cap "butt", "round", "square" Line cap. Default "butt".
join "miter", "round", "bevel" Line join. Default "miter".
miterLimit number Default 4.
align "center", "inside", "outside" Stroke alignment on closed outlines. Default "center".
double { "gap": 2, "innerColor": Paint } Draw two parallel lines (UML composition borders, "is-a" rails, road casings).
casing { "color": Paint, "width": 4 } Draw a wider line underneath (halo). Makes edges readable where they cross others.
effect LineEffect Geometric effect along the path (below).
flow { "speed": 20, "direction": "forward" } Animated dash movement along the path (data flow, active connections). Runtimes MUST honour reduced-motion preferences and MAY disable it.
sketch { "roughness": 1.2, "bowing": 1, "seed": {cel} } Hand-drawn rendering. The seed SHOULD be derived from the element id so the sketch is stable.

DashStyle is one of the named presets or an explicit array of alternating dash and gap lengths:

Preset Pattern (in multiples of width) Typical use
solid – Default
dashed [4, 3] Optional, dependency, inheritance of interfaces
long-dash [8, 4] Boundaries, planned items
short-dash [2, 2]
dotted [0.1, 2] with round cap Annotations, notes, traces
dash-dot [6, 3, 0.1, 3] with round cap Center lines, axes
dash-dot-dot [6, 3, 0.1, 3, 0.1, 3] Phantom lines
[n, …] explicit Anything else

LineEffect modifies the geometry of a line or outline:

type Parameters Result
wave amplitude, wavelength Sine wave along the path (signals, "wireless", uncertain dependencies).
zigzag amplitude, wavelength Zigzag (lightning, "breaks" on axes).
loops radius, spacing Coil (springs, inductors).
ticks length, spacing, side ("left", "right", "both") Tick marks along the line (railways, fences, boundaries).
arrows spacing, marker Repeated small arrowheads along the line (flow direction on long edges).
offset distance Parallel offset (lane markings).
plugin name, args Custom effect.

Effects apply to the path before dashes, so a dashed wave is possible. Effects never change the hit-test geometry, which follows the base path widened by hitWidth (6.10).

6.5 Text and fonts

A Font object:

Property Type Description
family Bindable string Font stack. Default { "token": "font.body" } or "system-ui, sans-serif".
size Bindable number Canvas units. Default 14.
minSizeScreenPx number Text below this on-screen size is hidden or replaced by a placeholder bar (level of detail).
weight 100–900 or "normal", "bold" Default 400.
style "normal", "italic"
color Paint Default { "token": "color.text" }.
lineHeight number Multiple of size. Default 1.3.
letterSpacing number Canvas units.
decoration "none", "underline", "line-through", "overline" UML static members are underlined, for instance.
transform "none", "uppercase", "lowercase", "capitalize"
variant "normal", "small-caps", "tabular-nums"
halo { "color": Paint, "width": 3 } Outline around glyphs for legibility over lines and fills.

Font files MAY be declared in notation.fonts: { "Inter": { "src": ["fonts/Inter.woff2"], "weights": [400, 600] } }. Runtimes that cannot load a font use the next family in the stack.

Text metric. notation.textMetric ($defs/TextMetric) says how text is measured wherever a measurement affects geometry. It is "host" (default, the 0.1 behaviour: the host's font metrics) or { "kind": "average", "advance": number, "count": "utf16" | "codepoint" | "grapheme", "lineHeight": number }: every counted character advances by advance times the font size, and a line is lineHeight times the font size high. When a metric other than host is declared, runtimes and headless layout MUST use it for every measurement that affects geometry (autoSize, wrapping at maxWidth, overflow: "ellipsis" and "shrink", and the inputs of layout), so that every host draws the same geometry; the glyphs themselves are still drawn in the real font. CEL reads the metric with textWidth and textHeight (12.4).

"textMetric": { "kind": "average", "advance": 0.55, "count": "utf16", "lineHeight": 1.4 }

A mind map sizes its topic boxes from textWidth(self.text, 14.0), so two hosts lay out the same map the same way.

6.6 Styles

A Style bundles visual properties. Styles are declared in notation.styles and referenced by name, or written inline.

Property Type Description
extends style name or name[] Inherit from other styles (merged left to right, then own properties).
fill Paint Area fill.
fillOpacity fraction
stroke Stroke Outline or line.
opacity fraction Whole-element opacity.
cornerRadius number or [tl, tr, br, bl] For shapes that support it.
shadow {dx, dy, blur, color} or "none" Drop shadow. Runtimes MAY omit shadows in low-power modes.
font Font Default font for labels of the element.
textAlign "left", "center", "right", "justify"
verticalAlign "top", "middle", "bottom"
padding Insets Inner spacing for labels and content.
cursor CSS cursor name Pointer cursor over the element.
filter "none", "grayscale", "blur" Visual filter (for disabled or ghost elements).
blend CSS blend mode
visible Bindable bool
{
  "styles": {
    "base":     { "fill": { "token": "color.surface" },
                  "stroke": { "color": { "token": "color.border" }, "width": { "token": "size.stroke" } },
                  "font": { "size": 13 } },
    "emphasis": { "extends": "base", "stroke": { "width": 2 }, "font": { "weight": 600 } },
    "ghost":    { "extends": "base", "opacity": 0.5, "stroke": { "dash": "dashed" } }
  }
}

Interaction states. Every node, edge, port and label notation MAY declare states, a map from state to Style:

State Active when
hover Pointer is over the element.
selected Element is in the selection.
focused Element has keyboard focus.
dragging Element is being moved or resized.
highlighted Highlighted by search, a finding link, a hook action or a related selection.
dropTarget A dragged element can be dropped into this container or connected to this element.
dropReject A dragged element cannot be dropped or connected here (a connect/containment constraint failed).
invalid The element has at least one finding with severity error.
warning The element has at least one finding with severity warning (and none with error).
disabled The element is read-only (locked, or enabled of its notation is false).
editing A label of the element is being edited inline.
filteredOut A canvas filter with effect: "dim" filters the element out (6.13.1).

Runtimes provide sensible defaults for hover, selected, dropTarget, dropReject, invalid and warning (for example a selection outline and a red finding badge) that apply when the specification declares none. A notation that declares a badge with the id finding (6.9) replaces the runtime's default finding badge on that kind of element, so a specification can place its own glyph, driven by self.findingSeverity() (12.2):

{ "id": "finding", "position": "bottom-right", "offset": [-10, -10],
  "text": { "cel": "self.findingSeverity() == 'error' ? '✖' : '⚠'" },
  "visible": { "cel": "self.findingSeverity() in ['error', 'warning']" } }

Conditional styles apply when an expression holds:

{
  "conditions": [
    { "when": "self.priority == 'high'", "style": { "stroke": { "color": { "token": "color.danger" }, "width": 2 } },
      "doc": "High-priority tasks have a red border." },
    { "when": "self.done", "style": "ghost" }
  ]
}

6.7 Shapes: built-in primitives

A node's body is a shape. A shape reference is either a name ("ellipse"), or an object with a type and parameters, where every parameter is Bindable:

{
  "shape": "roundedRect",
  "shape": { "type": "roundedRect", "params": { "radius": 12 } },
  "shape": { "type": "polygon", "params": { "sides": { "cel": "self.corners" }, "rotation": 0 } },
  "shape": { "type": "note", "params": { "fold": 14 }, "doc": "Folded corner signals a comment." }
}

The standard library offers these primitives (all parameters optional; defaults in Appendix B.2):

Shape Parameters Notes
rect –
roundedRect radius, radii [tl,tr,br,bl]
pill – Stadium; radius = half the smaller side.
ellipse – Circle when width = height.
superellipse exponent (≥ 2) The curve |2x/w − 1|ⁿ + |2y/h − 1|ⁿ = 1 with n = exponent; outline and text area as for ellipse, the text area inset where the curve crosses the diagonals.
circle – Always circular; size is min(width, height).
diamond – Rhombus touching the bounds' midpoints.
triangle apex (fraction along top edge), direction (up, down, left, right)
parallelogram skew (fraction or length), direction (right, left) Data, I/O.
trapezoid inset (fraction), direction Manual operations.
hexagon inset, orientation (flat, pointy) Preparation.
octagon inset
polygon sides, rotation, points (explicit fractional points) Regular or explicit polygon.
star points, innerRadius (fraction), rotation
cross thickness (fraction) Plus shape.
cylinder cap (height of the ellipse cap), orientation (vertical, horizontal) Databases, storage.
document wave (amplitude fraction) Wavy bottom.
multiDocument wave, offset, count Stacked documents.
note fold (size of the folded corner) Comments.
folder tabWidth, tabHeight, tabPosition (left, right) Packages.
frame tabWidth, tabHeight UML frames with name tab.
cloud bumps External systems.
process inset Predefined process (double side bars).
delay – D-shape.
display – Flowchart display.
manualInput slope Sloped top.
offPage pointer Pentagon pointing down.
callout pointerX, pointerY (fractions, may lie outside 0–1), pointerWidth, radius Speech bubble with a tail.
blockArrow direction, headLength, shaftThickness, doubleHeaded Arrow as a node.
chevron depth, direction Process steps.
actor – Stick figure; label below by default.
component – UML component icon rectangle.
line orientation (horizontal, vertical) A line node (separators, lifeline bodies).
bracket side, curl Braces for annotations.
text – No visible body; just the label (free text).
image src (Bindable URI), fit (contain, cover, fill, none), crop Image nodes.
icon icon (IconRef), padding Icon filling the node.
none – Invisible body; used for pure containers or label-only elements.

Every built-in shape defines its outline (for edge anchoring and hit testing) and its text area (where the main label goes). For example, a cylinder's text area excludes its top cap; an actor's text area lies below the figure.

"shape": { "type": "superellipse", "params": { "exponent": 4 } }

A functional decomposition graph draws its UI elements as a squircle with exponent 4, exactly rather than approximated by curves.

6.8 Custom shapes

When no primitive fits, a specification declares its own shapes in notation.shapes. A custom shape can be as simple as a static SVG path, or a fully parameterised, handle-editable shape whose geometry is computed with CEL.

Property Type Description
label, doc Shown in shape pickers and documentation.
params map → ShapeParam Parameters (below).
path PathDef A single path (below).
parts ShapePart[] A composite of several shapes (below).
svg string Inline SVG markup scaled into the bounds (static shapes only). MUST be sanitised (section 16).
plugin {name, args} Rendered by a plugin.
outline "path", "bounds", "ellipse", PathDef Geometry used for edge anchoring and hit testing. Default: the path, or the union of parts marked outline: true.
textArea Box Where the main label is laid out, in shape coordinates. Default: the bounds minus padding.
handles Handle[] Interactive handles that edit parameters (below).
defaultSize Size Suggested size when used without an explicit node size.
aspectRatio number Fixed aspect ratio, if any.
scaling ScaleMode How static geometry maps into bounds (below).

Exactly one of path, parts, svg or plugin MUST be present.

A custom shape whose name equals a built-in shape's name shadows the built-in within the specification, so a specification written before a built-in was added keeps drawing its own shape. Shapes that are neither a primitive nor worth one, such as a diode whose ends are rounded by min(w, h), are custom shapes with GeomExpr geometry, which is exact up to sampling.

Shape coordinate context. Geometry expressions (GeomExpr, 2.5c) are evaluated with these variables:

Variable Type Meaning
w, h double Width and height of the node's bounds in canvas units.
p map Resolved parameter values (p.fold).
self Element The model element (attributes available, read-only).
env Env Zoom, theme mode, locale.

Coordinates are relative to the top-left of the bounds; x grows right, y grows down, regardless of the coordinate system orientation.

ShapeParam

Property Type Description
type "number", "int", "bool", "enum", "color", "string" Required.
default value or {cel} Default value; CEL defaults may depend on w and h (for example "min(w, h) * 0.15").
min, max GeomExpr Bounds; may depend on w and h.
values string[] For enum.
unit "length", "fraction", "angle", "count" Semantics for property forms and handle behavior.
label, doc Shown in the shape parameter panel.
persist "view", "none" Whether user-adjusted values (via handles or the style panel) are stored per element in view data. Default "view".

PathDef — a path is either an SVG path string in a declared viewBox, scaled into the bounds, or a list of segments whose coordinates are GeomExprs:

{
  "path": { "viewBox": [0, 0, 100, 60], "d": "M0 10 Q50 -10 100 10 L100 50 Q50 70 0 50 Z" }
}
{
  "path": {
    "segments": [
      { "op": "M", "x": 0,            "y": 0 },
      { "op": "L", "x": "w - p.fold", "y": 0 },
      { "op": "L", "x": "w",          "y": "p.fold" },
      { "op": "L", "x": "w",          "y": "h" },
      { "op": "L", "x": 0,            "y": "h" },
      { "op": "Z" }
    ]
  }
}
op Fields Meaning
M x, y Move to.
L x, y Line to.
H, V x or y Horizontal or vertical line to.
Q cx, cy, x, y Quadratic Bézier.
C c1x, c1y, c2x, c2y, x, y Cubic Bézier.
A rx, ry, rotation, largeArc, sweep, x, y Elliptical arc (SVG semantics).
R x, y, w, h, r Rounded rectangle sub-path.
E cx, cy, rx, ry Ellipse sub-path.
Z – Close.

A path MAY have several sub-paths and a fillRule ("nonzero" default, "evenodd"). Each segment MAY carry when (Expression) to include it conditionally, which enables shapes whose topology depends on parameters (for example an optional double border).

ScaleMode controls how viewBox geometry and svg content fit the bounds: "stretch" (default), "uniform" (keep aspect ratio, centered), "none" (1:1 at the top-left), or { "nineSlice": [top, right, bottom, left] } — the corners of the viewBox keep their size and only the middle stretches, so rounded corners and tabs do not distort.

ShapePart — composite shapes combine parts, each of which is itself a shape (built-in or custom) with its own box and style. This is how an actor, a database with a label band, or a UML class header is composed.

Property Type Description
id identifier Name of the part; styles and states can address parts (partStyles).
shape ShapeRef The part's shape.
box {x, y, w, h} of GeomExpr Part bounds in shape coordinates.
style StyleRef Style of the part, merged over the node style.
rotate GeomExpr Rotation in degrees around the part's center.
when Expression Include only when true.
outline bool Part contributes to the connection outline.
hit bool Part is hit-testable. Default true.

Handles let users fine-tune a shape directly on the canvas — dragging the fold of a note, the tail of a callout, the skew of a parallelogram — like the yellow diamonds of classic drawing tools.

Property Type Description
param param name The parameter the handle edits. Required.
x, y GeomExpr Handle position in shape coordinates, computed from the current parameter values.
axis "x", "y", "both", "radial" Direction the handle moves in.
value Expression Inverse mapping: computes the new parameter value from the pointer position px, py (shape coordinates), for example "w - px". Default: the pointer coordinate along axis.
yParam, yValue param name, Expression For two-parameter handles (axis: "both").
snap SnapRule Snapping of the parameter value (for example { "grid": { "spacing": 2 } } or { "values": [0, 0.25, 0.5] }).
cursor, style Handle appearance.
label Message Tooltip while hovering or dragging ("Fold size: 14 px"); context handle (12.3), with p holding the parameter values under the pointer while dragging.
doc Doc
visible Expression Whether the handle is offered; context element. Default true.
write Action[] For a parameter bound with {cel}: the inverse, run with value (and yValue) bound to the new, snapped parameter value; context handleWrite (12.3).
refusals { "move": Reason } Shown when the user tries to drag the handle while it is not offered.

Handles that write the model. Where the shape instance binds a handle's parameter with { "attribute": path } (2.5b), dragging the handle MUST write that attribute, as the two-way binding of 2.5b says, and not view data. Where it binds the parameter with {cel}, the handle's write actions are run instead, like a computed placement's inverse (5.8); without write, the handle behaves as in 0.1 and its value is overridden by the binding, so a validator SHOULD warn about it. A handle on a free or {param} parameter keeps writing view data, as in 0.1.

  • A handle drag that writes the model MUST be one transaction, one undo step, of the gesture kind change, with attribute bound for each written attribute: change gesture constraints, outOfRange (4.3), hooks and invariants apply as to any other attribute change. A refused drag MUST return the handle to its previous position and show the refusal.
  • A parameter written through the model MUST NOT also be stored in view data; its persist is ignored.
  • A handle whose visible is false is not drawn. A runtime that lets the user try it anyway MUST refuse with refusals.move when it is declared.
  • The handle's snap MAY be a CEL snap rule in the handle context, which lets a handle land a step away from its neighbours.
"handles": [
  { "param": "b1", "x": "w * p.b1", "y": "h / 2", "axis": "x",
    "visible": "self.view.selected && env.viewpoint == 'trueTime' && visiblePhases(self) >= 2",
    "snap": { "cel": "boundaryLanding(self, 1, value)" },
    "write": [ { "set": { "peakEnd": "fractionToMonth(self, value)" } } ] }
]

A hype cycle's phase boundary is a handle on its banner that writes the month the peak ends, as a model edit, and is offered only between two visible phases.

Example — a callout with a draggable tail and adjustable radius.

{
  "shapes": {
    "speech": {
      "label": "Speech bubble",
      "doc": "Drag the round handle to point the tail, the square handle to change the corner radius.",
      "params": {
        "tipX":   { "type": "number", "unit": "fraction", "default": 0.2, "min": -0.5, "max": 1.5 },
        "tipY":   { "type": "number", "unit": "fraction", "default": 1.4, "min": -0.8, "max": 1.8 },
        "base":   { "type": "number", "unit": "fraction", "default": 0.3, "min": 0.1, "max": 0.8 },
        "radius": { "type": "number", "unit": "length", "default": 8, "min": 0, "max": "min(w, h) / 2" }
      },
      "parts": [
        { "id": "body", "shape": { "type": "roundedRect", "params": { "radius": { "param": "radius" } } },
          "box": { "x": 0, "y": 0, "w": "w", "h": "h" }, "outline": true },
        { "id": "tail",
          "shape": { "path": { "segments": [
            { "op": "M", "x": "w * p.base",        "y": "h - 1" },
            { "op": "L", "x": "w * p.tipX",        "y": "h * p.tipY" },
            { "op": "L", "x": "w * (p.base + 0.15)", "y": "h - 1" },
            { "op": "Z" } ] } },
          "box": { "x": 0, "y": 0, "w": "w", "h": "h" } }
      ],
      "textArea": { "x": 10, "y": 8, "w": "w - 20", "h": "h - 16" },
      "handles": [
        { "param": "tipX", "yParam": "tipY", "axis": "both",
          "x": "w * p.tipX", "y": "h * p.tipY",
          "value": "px / w", "yValue": "py / h", "label": "Tail tip" },
        { "param": "radius", "axis": "x", "x": "p.radius", "y": 0, "value": "px",
          "snap": { "grid": { "spacing": 2 } }, "label": "Corner radius" }
      ]
    }
  }
}

Example — a static path from a viewBox with nine-slice scaling.

{
  "ticket": {
    "path": { "viewBox": [0, 0, 120, 60],
              "d": "M8 0 H112 A8 8 0 0 0 120 8 V52 A8 8 0 0 0 112 60 H8 A8 8 0 0 0 0 52 V8 A8 8 0 0 0 8 0 Z" },
    "scaling": { "nineSlice": [8, 8, 8, 8] }
  }
}

6.9 Node notation

notation.nodes maps node type names to NodeNotation objects.

Property Type Description
shape ShapeRef Body shape. Default "rect".
style StyleRef or StyleRef[] Base style(s).
partStyles map part id → StyleRef Styles for parts of composite shapes.
size SizeSpec Default, minimum, maximum size and resize behavior (below).
placement Placement Coordinate binding (5.8).
snapping Snapping Per-type snapping override (5.9).
rotation {allowed, default, step} Whether nodes may rotate; rotation is stored in view data.
labels Label[] Text labels (6.12). If absent, one centered label bound to the type's labelAttribute.
compartments Compartment[] Stacked regions listing items (below).
form EmbeddedForm Form widgets drawn inside the node (7.6).
icon NodeIcon Icon placement (below).
badges Badge[] Small indicators at corners or edges (below).
badgeLayout BadgeLayout Slots that packed badges fill in order (below).
ports map port name → PortNotation Port visuals (below).
anchors AnchorSpec Where edges attach when no port is used (below).
container ContainerSpec How children are laid out and clipped (below).
variants Variant[] Conditional alternative notations (below).
lod LodRule[] Level of detail by zoom (below).
states map state → Style Interaction states (6.6).
conditions {when, style, doc}[] Conditional styles (6.6).
tooltip Message Hover text; context element. Default: the type's doc.summary.
layer "background", "default", "foreground", or int Rendering layer (6.16).
selectable, deletable, copyable, connectable Bindable bool Interaction permissions (default true).
refusals GestureRefusals Why a gesture this notation switches off is refused, per gesture (below).
hitPaddingScreenPx number Extra hit area around the outline.
doubleClick "editLabel", "openForm", "drillDown", "none", or operation name Double-click action. Default "editLabel".
drillDown {viewpoint, filter} Open a sub-diagram (for example a sub-process) when drilling down.
accessibility {role, name, description} Accessible name and description (6.15).
doc Doc Documents the notation itself ("Rounded corners mark states, circles pseudo-states").

GestureRefusals ($defs/GestureRefusals) maps a gesture to the Reason[] shown when a user attempts it on an element of this kind and the notation, placement or metamodel switches it off. Its keys are move (placement movable: false, any edge, a layout-placed element), resize, reparent, delete (deletable: false), connect (connectable: false), reconnect (reconnectable: false), copy (copyable: false) and editLabel (a label with editable: false). Reasons are evaluated in the element context. A runtime MUST show the applicable refusal when a user attempts that gesture on an element of that kind, and MUST NOT refuse silently when one is declared. A refusal here comes first in the order of gesture checks (8.4).

"nodes": {
  "Annotation": { "refusals": { "move": [ "An annotation stays with what it constrains; move that instead." ] } },
  "Variable":   { "refusals": { "reparent": [ "A query's structure comes from its text, so nothing here can be moved into a different group. Dragging changes where an element sits on the canvas." ] } }
}

SizeSpec

Property Type Description
default Size Size on creation.
min, max Size Limits (null for unbounded in one dimension).
fixed Size Shorthand for default = min = max and not resizable.
resizable bool, "horizontal", "vertical" Default true.
aspectRatio number or "keep" Fixed ratio or keep the ratio of the default size while resizing.
autoSize "none", "fitContent", "fitWidth", "fitHeight", "grow" Size follows content: labels, compartments, children, embedded forms. grow fits content but never shrinks below the user's size.
width, height Bindable number Computed or attribute-bound size in canvas units (for example a bar chart node). Placement width/x2 take precedence on bound axes.

Compartments are stacked horizontal regions inside a node that list items — class attributes and operations, table columns, BPMN lanes' contents, checklist items.

Property Type Description
id identifier Required.
title LocalizedText Optional header text.
items {attribute}, {children}, or {cel} Source list: a many attribute, children of certain types ({ "children": ["Column"], "slot": "columns" }), or a CEL list.
itemText Bindable string Text per item; the context adds item and index. For child elements, a label binding writes back.
itemIcon Bindable IconRef Icon per item.
itemStyle StyleRef Style of every item.
itemConditions {when, style}[] Conditional item styles; when sees item and index (for example primary-key columns underlined).
layout "vertical" (default), "horizontal", "grid", "wrap" Arrangement of items.
separator Stroke or "none" Line above the compartment.
size "auto" or number or {min, max} Height.
overflow "grow", "clip", "scroll", "ellipsis" When items exceed max. ellipsis shows "… 3 more".
maxItems int Items shown before overflow.
collapsible, collapsed bool, Bindable bool Collapse state persisted in view data.
emptyText LocalizedText Placeholder when empty.
editable {add, remove, reorder, inline} of bool In-place editing of items.
visible Bindable bool
padding, style

NodeIcon: { "icon": IconRef, "position": Position, "size": 16, "color": Paint, "flip": Bindable, "visible": Bindable }. Badge: { "id", "pack": bool, "position": Position, "offset": [dx, dy], "shape": ShapeRef, "text": Bindable string, "icon": IconRef, "flip": Bindable, "style": StyleRef, "size", "visible": Bindable bool, "tooltip", "onClick": operation } — for example a finding counter, a lock, a stereotype glyph, a progress ring. flip mirrors the icon: "none" (default), "horizontal", "vertical" or "both"; hit testing is unchanged.

Packed badges. BadgeLayout ($defs/BadgeLayout) is { "start": Position, "offset": [dx, dy], "direction": "left" | "right" | "up" | "down", "spacing": number }. Badges with pack: true whose visible holds MUST take consecutive slots in declaration order, the first at start moved by offset, each next one spacing canvas units further in direction; a badge that is not visible takes no slot. The position and offset of a packed badge are ignored. badgeLayout MAY also be set in a variant.

"badgeLayout": { "start": "top-right", "offset": [-12, 12], "direction": "left", "spacing": 16 },
"badges": [
  { "id": "disabled", "pack": true, "text": "⊘", "visible": { "cel": "!self.enabled" }, "tooltip": "Disabled: this will not run." },
  { "id": "manual",   "pack": true, "text": "▶", "visible": { "cel": "self.manual" } }
]

A pipeline step shows only the indicators that apply, leftward from its top-right corner, without gaps.

Positions inside a node are one of "center", "top", "bottom", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right", the same prefixed with outside- (for example "outside-bottom" below the node, as for actors and events), or an object { "anchor": [fx, fy], "offset": [dx, dy], "align": "center" }. The offset of the object form MAY be bound: { "attribute": path }, naming an attribute that holds [dx, dy] or a struct with dx and dy, or { "cel": … } returning [dx, dy]. An absent attribute value falls back to the notation's default position. A label whose offset is bound to an attribute writes that attribute when it is dragged (6.12).

Ports notation

Property Type Description
shape ShapeRef Port shape. Default "rect" 8 × 8.
size Size
style, states
side "top", "right", "bottom", "left", "auto", Bindable Side of the node. auto chooses by direction (in = left, out = right) in the layout direction.
position Bindable fraction or "distribute" Position along the side; distribute spaces all instances evenly. Default "distribute".
inset number Offset from the outline: negative = outside, 0 = centered on the outline, positive = inside.
movable bool Users may drag ports along the outline (positions stored in view).
label Label Port label; default position "outside" of the side.
order Expression Sort key for distributed ports.

AnchorSpec — when edges attach to the node itself (not a port):

Property Type Description
mode "outline", "center", "fixed", "sides" outline (default): intersection of the line towards the center with the outline; center: all edges aim at the center; fixed: only at listed points; sides: at the midpoint of the nearest side (orthogonal diagrams).
points {id, x, y}[] Fixed anchor points as fractions of the bounds.
gap number Distance between the outline and the edge end.
spread bool Distribute several edges arriving on the same side instead of converging on one point.
sides ("top", "right", "bottom", "left")[] The sides edges may attach to. Default: all four. With sides mode, the midpoint of the nearest allowed side is used; with outline, the attachment point is clamped to the allowed sides; fixed and center ignore it.
"anchors": { "mode": "sides", "sides": ["left", "right"] }

A mind map's branches leave a topic at the middle of its left or right side, never at the top or bottom.

ContainerSpec — for nodes whose type has children:

Property Type Description
layout "free", "stack", "grid", "flow", "lanes", "layout:<name>" Arrangement of children. lanes splits the container into bands (pools with lanes). layout:<name> delegates to a named layout (section 10).
direction "vertical", "horizontal" For stack, flow and lanes.
gap, padding number, Insets
columns int For grid.
header {size, side, label, style} Header band (title bar of a group, vertical name band of a BPMN pool on side: "left").
contentArea Box Area children are placed in, in node coordinates.
clip bool Clip children to the content area.
autoGrow bool Grow when children are moved to the edge.
collapsible bool Collapsing hides children and optionally switches to a collapsed variant.
dropZones {slot, box, style}[] Explicit regions that accept children of a slot (4.8).
highlightOnDrop bool Apply the dropTarget state while a valid child is dragged over.
nesting "inside", "none" Whether children are drawn inside the container. Default "inside", the 0.1 behaviour.

With nesting: "none", children MUST NOT be drawn inside the parent's bounds: the parent does not grow to enclose them (autoGrow, clip, contentArea, header and dropZones are ignored), child positions are not relative to the parent, and moving the parent does not move its children unless the layout does. Containment still governs parent, children, deletion (9.5), the clipboard and collapsible. The lines that show the containment are drawn by a derived relation (4.9), as a mind map draws its branches:

"container": { "layout": "layout:mindmap", "nesting": "none", "collapsible": true, "highlightOnDrop": true }

Variants switch whole parts of a notation based on model state — for example a collapsed sub-process, an event rendered differently when interrupting, a gateway symbol that depends on its kind:

{
  "variants": [
    { "when": "self.kind == 'exclusive'", "doc": "XOR gateway shows an ×.",
      "icon": { "icon": "std.x", "position": "center", "size": 18 } },
    { "when": "self.kind == 'parallel'",
      "icon": { "icon": "std.plus", "position": "center", "size": 18 } },
    { "when": "self.view.collapsed", "shape": "roundedRect", "size": { "fixed": [120, 60] }, "compartments": [] }
  ]
}

A Variant may contain any NodeNotation property except placement; matching variants are applied in order over the base notation.

Level of detail (lod) implements semantic zoom: [ { "maxZoom": 0.3, "hide": ["compartments", "badges", "form"], "labels": "primary", "shape": "rect" }, { "maxZoom": 0.1, "render": "box" } ]. hide lists notation properties or label ids; render: "box" draws a plain filled rectangle for performance.

6.10 Edge notation

notation.edges maps relation type names — and optionally Type.attribute names for reference attributes that should be drawn as edges — to EdgeNotation objects.

Property Type Description
line LineSpec Stroke, routing and geometry (below).
style StyleRef Style whose stroke and font apply to line and labels.
sourceMarker, targetMarker MarkerRef or Bindable Arrowheads and end decorations (6.11). Default: none at source, arrowFilled at target for directed relations.
midMarkers MidMarker[] Decorations along the edge (below).
labels EdgeLabel[] Labels at start, middle, end or any fraction (6.12).
anchoring {source, target} of EndAnchor How ends attach (below).
selfLoop SelfLoopSpec Geometry of self loops.
parallel {spread, mode} How parallel edges between the same nodes are separated: spread distance and mode ("offset", "curve", "bundle").
jumps JumpSpec How this edge crosses others.
snapping Snapping Bendpoint snapping override.
hitWidthScreenPx number Width of the hit area. Default 8.
layer "belowNodes", "aboveNodes", int Rendering layer (6.16). Default "belowNodes".
states, conditions, variants As for nodes; variants may replace line, markers and labels.
tooltip, selectable, deletable, reconnectable tooltip is a Message (context element). reconnectable controls whether users may drag an end to another element.
refusals GestureRefusals As for nodes (6.9); for example "move": ["An edge is drawn between its endpoints; move one of those instead."].
connect ConnectGesture The anchors a connect gesture of this relation type starts from, and which end each one makes (below).
stub Stub How a relation without a target is drawn (below). Also allowed in variants.
accessibility, doc

LineSpec

Property Type Description
stroke Stroke Everything from 6.4: color, width, dash, caps, double, casing, effect, flow, sketch.
routing Routing "straight" (default), "polyline" (user bendpoints), "orthogonal" (horizontal/vertical segments), "rounded" (orthogonal with rounded corners), "curved" (smooth curve through bendpoints), "bezier" (cubic with editable control points), "spline" (Catmull-Rom through points), "arc" (single circular arc), "tree" (fork from a common trunk), "metro" (octilinear: 0°/45°/90°), "layout" (from the layout algorithm), or { "plugin": … }.
cornerRadius number Rounding for orthogonal, metro and polyline.
curvature number For arc and curved (0 = straight, 1 = semicircle).
avoidNodes bool Route around nodes that are not endpoints.
avoidPadding number Clearance around avoided nodes.
bendpoints {editable, max, addOnDrag, removeOnStraighten} User editing of bendpoints; removeOnStraighten merges collinear points.
segments {editable, keepOrthogonal} Dragging whole segments of orthogonal edges.
minSegmentLength number Minimum length of the first and last segment (so markers do not sit on a corner).
startDirection, endDirection "auto", "up", "down", "left", "right", "normal" Direction the line leaves or enters its node; normal is perpendicular to the outline.
bezier BezierSpec Control points of a bezier route (below).

Curvature. For arc, and for curved without bendpoints, a positive curvature MUST bow the line to the left of the direction from source to target as drawn on screen (y down), and a negative one to the right, so that A → B and B → A with the same curvature bow to opposite sides. curved without bendpoints is one quadratic Bézier whose control point lies curvature × the chord's length from the chord's midpoint, perpendicular to the chord. arc is a circular arc whose sagitta is curvature × the chord's length / 2, so 1 is a semicircle.

"line": { "routing": "curved", "curvature": 0.2 },
"variants": [ { "when": "self.flipped", "line": { "curvature": -0.2 } } ]

Bézier reach. BezierSpec ($defs/BezierSpec) is { "reach": number | "auto", "backward": "direct" | "loop", "maxReach": number }. With routing: "bezier", the two control points MUST lie on the start and end directions (startDirection, endDirection), reach canvas units from each end; auto is half the distance between the ends along the start direction, and at least 30. With backward: "loop", when the end lies behind the start along the start direction (for right, the target anchor lies left of the source anchor), the reach becomes the larger of reach and half that distance plus the larger of the two nodes' heights (their widths, for up and down), capped by maxReach, so the curve leaves forward and comes back from behind instead of doubling over itself. Without bezier, the control points stay runtime-defined, as in 0.1.

"line": { "routing": "bezier", "startDirection": "right", "endDirection": "right", "bezier": { "reach": "auto", "backward": "loop" } }

JumpSpec: { "style": "none" | "arc" | "gap" | "square" | "sharp", "size": 6, "over": "later" | "horizontal" | "all" } — draws a small bridge where this edge crosses another. over decides which of two crossing edges jumps: the later-drawn one (default), always the horizontal one, or both.

EndAnchor: { "mode": "outline" | "center" | "port" | "fixed" | "sides" | "part", "gap": 2, "points": [...], "sides": [...] } overrides the node's AnchorSpec for this edge type; gap leaves space between the node outline and the marker, and sides restricts the sides as in AnchorSpec (6.9).

Ends bound to the model. With mode: "part", the end is a point on a side of a named part of the end node's shape (6.8), and the three values that say where can be bound to attributes of the relation:

Property Type Description
part Bindable string The id of a ShapePart of the end node's shape.
side Bindable "top", "right", "bottom", "left" The side of that part's box.
at Bindable fraction The position along that side, from its start (its left or top end).
movable bool The user may drag the end along the node's parts and sides. Default true when all three are attribute bindings.
default {part, side, at} The values written when a connect gesture does not determine them.

The end point MUST be the point at at along side of the part's current box, so resizing the node or changing its parameters moves the end in proportion. A connect gesture MUST write the bound attributes of both ends in the same transaction as the relation, and dragging an end MUST write them as one model edit, subject to constraints. When the named part is not drawn (its when is false), the edge MUST NOT be drawn; it stays in the model. The anchors of such an end are not written to EdgeView (DID, section 5).

"anchoring": {
  "source": { "mode": "part", "part": { "attribute": "fromPhase" }, "side": { "attribute": "fromEdge" }, "at": { "attribute": "fromAt" },
              "default": { "part": { "cel": "lastVisiblePhase(source)" }, "side": "bottom", "at": 0.5 } },
  "target": { "mode": "part", "part": { "attribute": "toPhase" }, "side": { "attribute": "toEdge" }, "at": { "attribute": "toAt" },
              "default": { "part": "peak", "side": "top", "at": 0.5 } }
}

An influence on a hype cycle ends at a phase, an edge of that phase and a fraction along it, all stored on the influence.

Direction chosen by the anchor. ConnectGesture ($defs/ConnectGesture) is { "from": map anchor id → "source" | "target", "tool": tool id }. from lists the anchors of the node notation (AnchorSpec points[].id) from which a connect gesture of this relation type starts, and which end the node dragged from becomes; without connect, every anchor starts one, as source (0.1). tool names a library tool (7.1) whose mode, initial, createSource and createTarget apply to a gesture started from an anchor.

  • A connect gesture started from an anchor listed in from MUST make the node dragged from the end named there, and the node it lands on the other end; connect gesture constraints (8.4) see source and target after this assignment.
  • createSource and createTarget (7.2) keep their meaning by role: when the gesture lands on empty canvas, the runtime creates the missing end, so a gesture from a "target" anchor uses createSource.
  • An anchor not listed in from MUST NOT start a connect gesture of this relation type.
"DependsOn": { "connect": { "from": { "right": "source", "left": "target" }, "tool": "dependsOn" } }

In a dependency graph, dragging from a node's right anchor makes it depend on the node dropped on; dragging from its left anchor makes that node depend on it.

Stubs. Stub ($defs/Stub) is { "length": number, "side": "right" | "left" | "top" | "bottom" | "auto", "style": StyleRef, "label": Label }. A relation whose target is absent (an optional target end, 4.9) MUST be drawn as a stub: a line of length canvas units leaving the source at the middle of side, with its style and label. The stub is the relation: selecting it selects the relation, and dragging its free end onto an element is a connect gesture. A relation without a target and without a stub is not drawn.

"UsesRole": { "stub": { "length": 48, "side": "right",
  "style": { "stroke": { "color": { "token": "color.warning" }, "dash": [3, 3], "dashScale": "absolute" } },
  "label": { "id": "asWritten", "text": { "cel": "self.targetAsWritten + ' (missing)'" }, "side": "above" } } }

SelfLoopSpec: { "shape": "arc" | "rect" | "circle", "size": 30, "side": "top" | "right" | "bottom" | "left" | "auto", "spread": 10 }.

MidMarker — decorations anywhere along the path:

Property Type Description
marker MarkerRef The decoration.
at fraction, "middle", "start", "end" Position along the path length. Default "middle".
repeat {spacing, from, to} Repeat every spacing canvas units within the range.
orient "auto", "auto-reverse", angle Rotation relative to the path tangent.
visible Bindable bool

Example — a richly decorated edge.

{
  "edges": {
    "Association": {
      "line": {
        "stroke": { "color": { "token": "color.border" }, "width": 1.25,
                    "dash": { "cel": "self.derived ? 'dashed' : 'solid'" },
                    "casing": { "color": { "token": "color.surface" }, "width": 5 } },
        "routing": "rounded", "cornerRadius": 6, "avoidNodes": true,
        "bendpoints": { "editable": true, "removeOnStraighten": true },
        "minSegmentLength": 16
      },
      "jumps": { "style": "arc", "size": 5 },
      "sourceMarker": { "cel": "self.sourceAggregation == 'composite' ? 'diamondFilled' : (self.sourceAggregation == 'shared' ? 'diamond' : 'none')" },
      "targetMarker": { "cel": "self.navigable ? 'arrow' : 'none'" },
      "labels": [
        { "id": "name",   "text": { "attribute": "name" }, "at": "middle", "side": "above", "editable": "inline" },
        { "id": "srcRole","text": { "attribute": "sourceRole" }, "at": "start", "offset": 12, "side": "above", "editable": "inline" },
        { "id": "srcMul", "text": { "attribute": "sourceMultiplicity" }, "at": "start", "offset": 12, "side": "below" },
        { "id": "tgtRole","text": { "attribute": "targetRole" }, "at": "end", "offset": 12, "side": "above", "editable": "inline" },
        { "id": "tgtMul", "text": { "attribute": "targetMultiplicity" }, "at": "end", "offset": 12, "side": "below" }
      ],
      "states": { "selected": { "stroke": { "color": { "token": "color.accent" }, "width": 2 } } }
    }
  }
}

6.11 Markers (arrowheads and end decorations)

A marker is drawn at an edge end (or along the edge) and oriented along the path tangent. References are a name, a name with parameters, or an inline declaration:

{
  "targetMarker": "arrowFilled",
  "targetMarker": { "type": "arrow", "params": { "length": 12, "width": 10, "angle": 30 } },
  "targetMarker": { "type": "stack", "markers": ["bar", "erMany"], "spacing": 3 },
  "targetMarker": { "cel": "self.optional ? 'erZeroOrMany' : 'erOneOrMany'" }
}

Built-in markers (details and default sizes in Appendix B.3):

Group Markers
Arrows arrow (open V), arrowFilled (solid triangle), arrowHollow (outlined triangle, UML generalisation), arrowConcave (swept-back barbed head), arrowThin, arrowDouble (two heads), halfArrowTop, halfArrowBottom (asynchronous messages), arrowCircle
Geometric diamond, diamondFilled, circle, circleFilled, square, squareFilled, dot, triangle, triangleFilled
Lines bar (perpendicular line), doubleBar, cross (×), slash
Crow's foot (ER) erOne, erOnlyOne, erMany, erZeroOrOne, erOneOrMany, erZeroOrMany
UML and engineering ballSocketBall (provided interface), socket (required interface), containment (circled plus), ground, fork
Other none

Marker parameters (all optional, Bindable):

Parameter Description
length Size along the path (canvas units).
width Size across the path.
angle Half-opening angle of arrows, in degrees.
scaleWithStroke Scale with the line width (default true, relative to width 1).
fill Paint, or "stroke" (same as line color, default for filled markers) or "background" (canvas or token color.surface, default for hollow markers so the line does not show through).
stroke Stroke of the marker outline; default inherits the line's color and width, never its dash.
inset Distance from the edge end point (move the marker back).
shorten How far the line is shortened so it ends inside the marker rather than poking through its tip. Default: computed per marker.
minSizeScreenPx Minimum on-screen size when zoomed out.

Custom markers in notation.markers:

Property Type Description
path PathDef Geometry in marker coordinates: x points along the path tangent towards the end, the tip is at the reference point.
parts ShapePart[] Composite marker.
ref [x, y] Reference point placed exactly at the edge end. Default [0, 0].
size [length, width] Nominal size (scales viewBox geometry).
params map → ShapeParam Parameters, used in GeomExprs as p.<name> (plus sw = stroke width).
orient "auto", "auto-start-reverse", angle Default "auto"; source markers are automatically reversed.
fill, stroke Paint, Stroke Defaults as above.
shorten GeomExpr Line shortening.
label, doc Shown in marker pickers and generated docs.
{
  "markers": {
    "chevronPair": {
      "label": "Double chevron",
      "doc": "Signals a streamed (continuous) flow.",
      "params": { "gap": { "type": "number", "default": 4 } },
      "path": { "segments": [
        { "op": "M", "x": -10, "y": -5 }, { "op": "L", "x": 0,           "y": 0 }, { "op": "L", "x": -10,          "y": 5 },
        { "op": "M", "x": "-10 - p.gap", "y": -5 }, { "op": "L", "x": "-p.gap", "y": 0 }, { "op": "L", "x": "-10 - p.gap", "y": 5 } ] },
      "fill": "none",
      "shorten": 0
    }
  }
}

Stacked markers (type: "stack") compose several markers from the end inward with a spacing, for example bar + erMany for "one or many" in some ER dialects, or an arrow followed by a circle.

6.12 Labels

Labels display text on nodes, edges, ports and compartments. The same Label object is used everywhere; edges add path-relative positioning.

Property Type Description
id identifier Unique within the notation; used in view data (label offsets), lod.hide and styles.
text Bindable string or { "attribute": … } Content. Attribute bindings make the label editable.
format "plain", "markdown", "rich" markdown renders a safe inline subset (emphasis, code, links); rich enables spans from { "cel": … } returning a list of {text, style}.
placeholder LocalizedText Shown (dimmed) when text is empty and the element is selected.
editable false, "inline", "multiline", "form" How users edit the text; form opens the field in the form. Requires an attribute binding. Default: "inline" for attribute bindings, else false.
parse {cel, write} Parse edited text into several attributes (for example "name : Type" → name, type), with value = entered text and write actions (9.4).
position Position For nodes and ports (6.9).
style StyleRef Font, color, alignment.
background {fill, stroke, padding, cornerRadius} Box behind the text (tags, pills).
maxWidth number or "parent" Wrap width.
wrap "none", "word", "char" Default "word" for node labels, "none" for edge labels.
maxLines int
overflow "visible", "clip", "ellipsis", "shrink" shrink reduces font size down to font.minSizeScreenPx.
rotation angle or "vertical" Vertical text for lane headers.
icon IconRef Icon before the text.
visible Bindable bool
draggable bool Users may move the label; the offset is stored in view data. Default true for edge labels, false for node labels.
link Bindable URI Clickable label.
layer "top" Draw the label above all elements (6.16) so it is never covered.
tooltip, doc

Label offsets in the model. When a node label's position has an offset bound to an attribute (6.9), dragging the label (where it is draggable) MUST write that attribute as a model edit, with undo and constraints, instead of view data; a label whose offset is bound to a readOnly or fixed attribute is not draggable.

"labels": [ { "id": "name", "text": { "attribute": "name" },
  "position": { "anchor": [1, 0.5], "offset": { "cel": "has(self.labelOffset) ? [self.labelOffset.dx, self.labelOffset.dy] : [15.0, 4.0]" } },
  "draggable": false } ]

A Wardley map reads each component's label offset from the map text, and draws the label at the default offset where the text gives none.

Edge label positioning — for labels in EdgeNotation.labels:

Property Type Description
at "start", "middle", "end", or fraction Anchor along the path. start and end are measured from the visible ends (after markers). Default "middle".
offset number Distance along the path from the anchor, in canvas units (towards the middle for start/end).
side "above", "below", "left", "right", "on" Side of the line relative to its direction. on centers the label on the line (with a background that interrupts it).
distance number Perpendicular distance from the line. Default 4 plus half the label height.
orientation "horizontal", "follow", "upright" Keep text horizontal (default), rotate along the path, or rotate but never upside down.
avoidMarkers bool Push start/end labels clear of markers. Default true.
keepOnSegment bool For orthogonal routes, keep start/end labels on the first/last segment.

Together, at and side place labels in any of the conventional slots:

Together, at and side place labels in any of the conventional slots:
Diagram as text (Mermaid)
block-beta
  columns 5
  startAbove["start-above"] space middleAbove["middle-above"] space endAbove["end-above"]
  start(("start")) space middle["middle"] space finish(("end"))
  startBelow["start-below"] space middleBelow["middle-below"] space endBelow["end-below"]
  start --- middle
  middle --> finish
  classDef slot fill:none,stroke:none
  class startAbove,middleAbove,endAbove,startBelow,middleBelow,endBelow slot

6.13 Canvas

notation.canvas (or a viewpoint's canvas) configures the drawing surface:

Property Type Description
background Paint Canvas background (default token color.canvas).
page {size, orientation, margins, showBreaks} Paged canvas for printing: size "A4", "Letter", [w, h] in mm.
bounds Box Fixed drawing area.
fit "none", "stretch" Whether the drawing area is stretched to the pane (below). Default "none".
margin Insets Space kept around the stretched bounds. Default 0.
zoom {min, max, default, steps, initial, fitPadding, fitWhen} Zoom limits and presets, and the zoom a viewer starts with (below).
watermark Label Text drawn under everything (for example "DRAFT" when diagram.status == 'draft').
legend {visible, position, entries, title, from, computed} Auto-generated legend of types, markers and conditional styles that carry doc (below).
minimap bool Show an overview map.
selection {style, handles} Selection outline and resize handle appearance.
title Label A title above the drawing surface (below).
header ChromeBand A band of rows between the title and the drawing surface (below).
notices Notice[] Status notices outside the drawing (below).
filters map id → Filter User filters, whose values are viewer state (6.13.1).
empty EmptyMessage The message shown on an empty canvas (below).

Chrome. The title, the header, notices, filter controls, variant toggles (3.5), a legend at an outside- position and the empty-canvas message are chrome: they are drawn outside the drawing surface, are not zoomed or panned, and are not elements. A runtime MUST NOT let them be selected, moved, deleted or included in layout or in the model's export. Their texts are Messages evaluated in the chrome context (12.3), which binds diagram and env and may read diagram.drawn, budget() and filterValue(); the title and header rows are Labels evaluated with self bound to the diagram.

  • Title. title is drawn above the drawing surface. editable works as for any label, and defaults to false when the text is bound to a derived attribute.
  • Header. header ($defs/ChromeBand) is { "rows": Label[], "style": StyleRef, "visible": Bindable bool }, drawn between the title and the drawing surface; the surface's coordinate origin lies below it.
  • Notices. A Notice ($defs/Notice) is { "id", "text": Message, "severity": "info" | "warning" | "error", "position": "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right", "visible": Bindable bool, "actions": [{label, operation, args}], "dismissible": bool, "style" }, with severity info and position top by default. Visible notices MUST stack in declaration order per position, and MUST be exposed to assistive technology as a status region. Each button runs its operation (9.3) with args evaluated in the chrome context. A dismissal is viewer state. Two notices are built in: each truncating budget contributes budget:<id> (3.2.1) while it truncates, and a parse failure of the primary file (8.6) contributes unavailable, whose text is the finding's message. A notice id is a simple identifier or budget:<id> naming a declared budget. A declared notice with the id of a built-in one (budget:<id> or unavailable) MUST replace it: it is shown only while the built-in one would be, and its own visible narrows that further. A declared budget:<id> notice replaces the notice whatever that budget's notice says. The 0.1 watermark keeps its meaning.
  • Empty canvas. empty ($defs/EmptyMessage) is { "text": Message, "when": Expression, "style": StyleRef }, drawn centred on the surface while when holds. The default when is that the view draws no element after steps 1 and 2 of the drawing order (6.1), before viewer filters and budgets, so a canvas emptied by a filter does not claim that the model is empty. The toolbox and empty-canvas gestures stay available.
  • Stretching. With fit: "stretch", bounds MUST be set, and the runtime MUST scale x and y independently so that bounds fills the pane minus margin, again when the pane is resized. Node sizes, strokes, fonts and label offsets are not stretched: they stay in canvas units at zoom 1, placed at their stretched anchor. User zoom and pan MAY be disabled. Stored positions are domain values and are unaffected.
  • Initial zoom. zoom.initial is a number or "fit"; default: zoom.default, as in 0.1. With "fit", the runtime MUST fit zoom and pan to the drawn content once per viewer and view, keeping fitPadding (a fraction of the content's extent, default 0.05) on each side, when the view opens or, with fitWhen: "firstContent" (default), the first time it has content; afterwards a model change MUST NOT change zoom or pan. A stored viewport (11.6) takes precedence, and an empty view uses default. The fitted value is viewer state.
  • Legend. With legend.from: "drawn", an entry MUST be shown only while at least one element of diagram.drawn matches it: a string entry of entries matches when a drawn element has that type, takes that enum value, uses that marker or satisfies that conditional style. With from: "declared" (default), every entry is shown, as in 0.1. legend.computed ($defs/LegendComputed) computes entries from the drawn elements: key (Expression, legend context) groups them into one entry each; label (Message) and swatch ({shape, fill, stroke, icon}) are evaluated on the first element of each group; order sorts the entries (default: first appearance in model order). legend.title is a Message heading the legend.
"canvas": {
  "title": { "id": "heading", "text": { "attribute": "heading" }, "style": "diagramTitle" },
  "legend": { "visible": true, "position": "outside-bottom", "from": "drawn",
    "computed": { "key": "self.kindTag + (self.external ? ' (external)' : '')",
                  "label": { "cel": "self.kindTag + (self.external ? ' (external)' : '')" },
                  "swatch": { "shape": "roundedRect", "fill": { "cel": "kindBackground(self)" } } } },
  "notices": [ { "id": "ambientHidden", "position": "bottom-left",
    "visible": { "cel": "!filterValue('ambient') && diagram.nodesOfType('Package').exists(p, p.isAmbient)" },
    "text": { "cel": "string(diagram.nodesOfType('Package').filter(p, p.isAmbient).size()) + ' ambient packages are hidden.'" },
    "actions": [ { "label": "Show them", "operation": "showAmbient" } ] } ],
  "empty": { "text": "This causal loop diagram states no variables yet." },
  "zoom": { "default": 1, "initial": "fit", "fitPadding": 0.1 }
}

The title and key of a C4 view, the ambient-package notice of a .NET dependency graph, the empty message of a causal loop diagram and a timeline's fit-once zoom, together in one canvas.

6.13.1 Filters

canvas.filters maps a filter id to a Filter ($defs/Filter), a control on the canvas chrome with which a viewer hides or dims elements. Its value is viewer state (the view-data kind filters, 11.6), whatever persistence.view.viewer lists.

Property Type Default Description
label Message the id Shown on the control.
control "chips", "switch", "select", "search" required chips: a list of chosen values; switch: a bool; select: one of options; search: free text.
appliesTo TypeRef[] every node type The types the filter can hide; others always stay.
options {cel} – The values offered by chips and select; context filter, without self.
default literal or {cel} [], false or '' The value a viewer starts with.
match { "default": "any" | "all", "userToggle": bool } { "default": "any", "userToggle": true } For chips: whether an element must match any or all chosen values, and whether the viewer may switch.
keep Expression → bool required True when self stays drawn; context filter.
effect "hide", "dim" "hide" dim applies the filteredOut state (6.6) instead of hiding.
position Position the runtime's Where the control sits, outside the drawing surface.
doc Doc

The filter context binds self, value (the filter's current value), match ("any" or "all"), diagram and env (12.3); filterValue(id) (12.4) reads a filter's value in the element and chrome contexts.

  • An element of a type in appliesTo for which keep is false MUST NOT be drawn, or, with effect: "dim", MUST take the filteredOut state. A relation with an end that is not drawn is not drawn (6.1).
  • Filtering MUST NOT change the model, the findings or the undo history. Filtered-out elements are excluded from diagram.drawn and from a legend with from: "drawn"; a message or reason that names a filtered-out element still names it (2.3).
"filters": {
  "tags": {
    "label": "Filter by tags", "control": "chips", "appliesTo": ["Trend", "Trigger"],
    "options": { "cel": "diagram.nodes.filter(n, n.isA('Trend') || n.isA('Trigger')).map(n, n.tags).flatten().map(t, lower(t)).distinct()" },
    "match": { "default": "any", "userToggle": true },
    "keep": "value.size() == 0 || (match == 'any' ? value.exists(t, self.tags.exists(s, lower(s) == lower(t))) : value.all(t, self.tags.exists(s, lower(s) == lower(t))))"
  }
}

A hype cycle's tag chips hide the trends and triggers without the chosen tags, and every influence to or from them; notes always stay.

6.14 Icons

notation.icons declares icons used by tools, nodes, badges and labels.

Form Example
Inline SVG { "svg": "<svg viewBox='0 0 24 24'>…</svg>" } (sanitised)
Path { "path": { "viewBox": [0,0,24,24], "d": "M…" }, "stroke": true }
Image { "src": "icons/db.png", "size": [24, 24] }
Icon font glyph { "font": "tabler", "glyph": "database" }
Library "std.database" or "tabler:database"

Icons are monochrome by default and take currentColor, so they follow the theme; colored: true keeps their own colors.

Wherever an IconRef is accepted, { "icon": IconRef, "flip": "none" | "horizontal" | "vertical" | "both" } mirrors the icon; flip is Bindable and does not change hit testing.

"icon": { "icon": "mdi-sync", "position": "top", "size": 26, "flip": { "cel": "self.kind == 'reinforcing' ? 'none' : 'horizontal'" } }

A causal loop turns its loop arrow clockwise for a reinforcing loop and anticlockwise for a balancing one.

6.15 Accessibility

Notations SHOULD make diagrams accessible:

  • accessibility.name (Bindable string) names each element for screen readers; the default is the type label plus labelAttribute.
  • accessibility.description describes relations (default: "Transition from Idle to Running, trigger start").
  • Runtimes SHOULD provide keyboard navigation between elements and along edges, and expose the model as an accessible tree.
  • Color MUST NOT be the only carrier of meaning in built-in defaults; specifications SHOULD pair color with shape, pattern, dash, icon or text. Validators MAY warn when two types differ only in fill color.
  • Themes SHOULD provide a high-contrast mode; runtimes SHOULD honour reduced-motion settings (disable flow animations).
  • Specifications SHOULD state the contrast their text needs with theme.contrast (6.2), with a minRatio of at least 4.5 for text against the fills it is drawn on.

6.16 Rendering order

Elements are drawn in layers: canvas background → grid and bands → background layer nodes → edges with layer: "belowNodes" → default nodes (containers before their children; siblings by zIndex in view data, then by stored order) → edges with layer: "aboveNodes" → foreground nodes → labels that are marked layer: "top" → selection, handles, guides and snapping feedback. Integer layers sort numerically between these named layers (background = −100, default = 0, foreground = 100).

Illustrated by

Referenced from