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.
{
"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:
- Per-element style override stored in the DID definition's view data (11.6), if the specification allows overrides.
- Interaction state styles (
states.selected,states.hover, …) that are active, in the fixed prioritydragging>selected>focused>hover>highlighted>dropTarget>invalid>warning>disabled. - Conditional styles (
conditions) whosewhenexpression is true, in declaration order (later wins). - Variants (
variants) whosewhenis true — they may replace shape, size, labels and style. - The notation's own
style(inline or referenced), withextendschains resolved. - The notation of the nearest supertype (C3 order), repeating steps 2–5.
- The viewpoint override and then the global
notation.defaultsfor the element kind. - 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:
- Viewpoint membership: the elements whose type the viewpoint includes and does not exclude, narrowed by its
memberswhen it declares them (3.5). - Derived elements: the derived nodes and relations (4.11) computed from the model are added.
- 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.
- Budgets: when a hard budget (3.2) is exceeded, the elements it cuts are removed, so a budget applies to what the filters leave.
- Visibility: elements whose resolved notation has
visible: falseare 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).
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.
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.
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).
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.
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" }
]
}
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.
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, withattributebound for each written attribute:changegesture 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
persistis ignored. - A handle whose
visibleis false is not drawn. A runtime that lets the user try it anyway MUST refuse withrefusals.movewhen it is declared. - The handle's
snapMAY be a CEL snap rule in thehandlecontext, 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] }
}
}
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.
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
fromMUST make the node dragged from the end named there, and the node it lands on the other end;connectgesture constraints (8.4) seesourceandtargetafter this assignment. createSourceandcreateTarget(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 usescreateSource.- An anchor not listed in
fromMUST 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 } } }
}
}
}
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.
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:
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 slotnotation.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.
titleis drawn above the drawing surface.editableworks as for any label, and defaults tofalsewhen 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" }, withseverityinfoandpositiontopby 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) withargsevaluated in thechromecontext. A dismissal is viewer state. Two notices are built in: each truncating budget contributesbudget:<id>(3.2.1) while it truncates, and a parse failure of the primary file (8.6) contributesunavailable, whose text is the finding's message. A noticeidis a simple identifier orbudget:<id>naming a declared budget. A declared notice with the id of a built-in one (budget:<id>orunavailable) MUST replace it: it is shown only while the built-in one would be, and its ownvisiblenarrows that further. A declaredbudget:<id>notice replaces the notice whatever that budget'snoticesays. The 0.1watermarkkeeps its meaning. - Empty canvas.
empty($defs/EmptyMessage) is{ "text": Message, "when": Expression, "style": StyleRef }, drawn centred on the surface whilewhenholds. The defaultwhenis 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",boundsMUST be set, and the runtime MUST scale x and y independently so thatboundsfills the pane minusmargin, 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.initialis 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, keepingfitPadding(a fraction of the content's extent, default 0.05) on each side, when the view opens or, withfitWhen: "firstContent"(default), the first time it has content; afterwards a model change MUST NOT change zoom or pan. A storedviewport(11.6) takes precedence, and an empty view usesdefault. The fitted value is viewer state. - Legend. With
legend.from: "drawn", an entry MUST be shown only while at least one element ofdiagram.drawnmatches it: a string entry ofentriesmatches when a drawn element has that type, takes that enum value, uses that marker or satisfies that conditional style. Withfrom: "declared"(default), every entry is shown, as in 0.1.legend.computed($defs/LegendComputed) computes entries from the drawn elements:key(Expression,legendcontext) groups them into one entry each;label(Message) andswatch({shape, fill, stroke, icon}) are evaluated on the first element of each group;ordersorts the entries (default: first appearance in model order).legend.titleis 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.
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
appliesTofor whichkeepis false MUST NOT be drawn, or, witheffect: "dim", MUST take thefilteredOutstate. 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.drawnand from a legend withfrom: "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.
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.
Notations SHOULD make diagrams accessible:
accessibility.name(Bindable string) names each element for screen readers; the default is the type label pluslabelAttribute.accessibility.descriptiondescribes 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-contrastmode; runtimes SHOULD honour reduced-motion settings (disableflowanimations). - Specifications SHOULD state the contrast their text needs with
theme.contrast(6.2), with aminRatioof at least 4.5 for text against the fills it is drawn on.
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
- C4 container diagram: 23 items in this layer
- Causal loop diagram: 12 items in this layer
- Databricks job: 17 items in this layer
- Entity–relationship diagram: 4 items in this layer
- Functional decomposition graph: 24 items in this layer
- Gartner hype cycle graph: 15 items in this layer
- Mind map: 14 items in this layer
- RDF graph: 13 items in this layer
- State machine: 15 items in this layer
- Project timeline: 6 items in this layer
Referenced from
- 2. Foundations
- 3. Specification structure, layers and viewpoints
- 4. Layer 1 — Metamodel
- 5. Layer 2 — Coordinate systems, placement and snapping
- 7. Layer 4 — Toolbox, context tools and forms
- 8. Layer 5 — Constraints
- 9. Layer 6 — Behavior
- 11. Layer 8 — Persistence
- 12. The CEL environment
- 14. Processing model
- 17. Complete examples
- Appendix B — Built-in catalogues
- Appendix D — Design rationale and open questions