Skip to content

5. Layer 2 — Coordinate systems, placement and snapping

Contents of DISL 0.8

Most diagram tools assume a single, infinite pixel plane. Many real diagrams do not fit that assumption: a Gantt chart places tasks along time; a swimlane diagram places activities in categories (lanes); a sequence diagram orders messages along a logical sequence; a floor plan measures in millimetres; a radial mind map uses angles and radii. DISL makes the coordinate system an explicit layer so that positions keep their meaning, can be bound to model data, and can be snapped in domain units.

5.1 Concepts

  • An axis maps domain values (numbers, timestamps, categories) to canvas distance along one direction. It is a scale in the sense of charting libraries.
  • A coordinate system combines two axes (x and y, or angle and radius) and defines orientation, bounds and the grid display.
  • Placement (5.8) defines, per node type, where each coordinate of an element comes from: stored freely in the view, bound to a model attribute (two-way), or computed by an expression (read-only).
  • Snapping (5.9–5.11) restricts positions and sizes to discrete values, independently per axis, in the axis's own units.
  • Canvas units are the internal unit of rendering at zoom 1. The screen maps canvas units to device pixels via the zoom factor.
From domain value to screen pixels. An axis scale maps a domain value, such as the date 2026-10-05, to canvas units, such as x = 480. The zoom maps canvas units to screen pixels, such as x = 720 at zoom 1.5. Snapping works on the domain value, in domain units, and the result is then mapped back to canvas units.
Diagram as text (Mermaid)
flowchart LR
  accTitle: From domain value to screen pixels
  accDescr: An axis scale maps a domain value, such as the date 2026-10-05, to canvas units, such as x = 480. The zoom maps canvas units to screen pixels, such as x = 720 at zoom 1.5. Snapping works on the domain value, in domain units, and the result is then mapped back to canvas units.
  domain["<b>domain value</b><br/>#quot;2026-10-05#quot;"]
  canvas["<b>canvas units</b><br/>x = 480"]
  screen["<b>screen pixels</b><br/>x = 720 at zoom 1.5"]
  domain -- axis.scale --> canvas
  canvas -- zoom --> screen
  canvas -. "snapping works here<br/>(in domain units, then mapped back)" .-> domain

5.2 The coordinates object

{
  "coordinates": {
    "axes": { "px": { "kind": "linear", "unit": "px" } },
    "systems": {
      "canvas": {
        "kind": "cartesian",
        "x": "px", "y": "px",
        "orientation": "y-down",
        "grid": { "visible": true, "style": "dots", "spacing": 10, "majorEvery": 5 },
        "snapping": "grid10"
      }
    },
    "snapProfiles": { "grid10": { "x": { "grid": { "spacing": 10 } }, "y": { "grid": { "spacing": 10 } } } },
    "default": "canvas"
  }
}
Property Type Description
axes map → Axis Named axes; may also be written inline inside systems.
systems map → CoordinateSystem Named coordinate systems.
snapProfiles map → Snapping Reusable snapping definitions.
default name System used by viewpoints that do not name one.
doc Doc

If coordinates is absent, the default is a single cartesian system canvas with two linear pixel axes, y pointing down, unbounded, grid hidden and snapping off.

5.3 Axes: common properties

Property Type Description
kind "linear", "log", "time", "ordinal", "angular" Required.
label LocalizedText Axis title, shown on rulers and headers.
doc Doc Explains to users what the axis means ("Calendar time in the project's time zone").
min, max domain value Domain bounds; placements outside are rejected by a built-in placement constraint.
reversed bool Domain increases towards negative canvas direction (for example values increasing upward in a y-down system).
origin domain value The domain value mapped to canvas coordinate 0.
ruler Ruler Ruler or header display (5.13).
expand "none", "auto" Whether the axis domain grows automatically when elements are placed beyond it. Default "auto" for unbounded axes.
outOfRange OutOfRange Whether a gesture or typed value past min or max is clamped or refused (4.3). Default: refused both ways, as in 0.1.
ranges AxisRange[] Named ranges of the domain, drawn as bands (below).
endLabels {min, max} of Message Labels at the two ends of the axis line or ruler, by domain end.

Out of range on an axis. An axis outOfRange works as on an attribute (4.3), with std.axisBounds in place of std.facets: with drag: "clamp", a dragged element stops at the bound, and the bound is applied after snapping, so it wins over the snap grid.

Axis ranges. An AxisRange ($defs/AxisRange) names a part of the domain: id (required), label (Message), from and to (required, domain values, from included and to excluded except at the axis maximum), fill (Paint), style (StyleRef), separator (Stroke, the line drawn at from) and labelPosition ("start", "center" (default) or "end"). Ranges MAY be unequal and need not be contiguous. They are drawn with the grid (6.16), under every element, and are not elements: they cannot be selected, and take no part in layout or findings. axisRange(axis, value) (12.4) returns the id of the range holding a value, so an attribute can derive a stage from the axis instead of repeating its numbers.

"maturity": { "kind": "linear", "label": "Evolution", "min": 0, "max": 1,
  "ranges": [
    { "id": "genesis",   "label": "Genesis",              "from": 0,     "to": 0.175, "style": { "fillOpacity": 0.35 } },
    { "id": "custom",    "label": "Custom Built",         "from": 0.175, "to": 0.4,   "style": { "fillOpacity": 0.5 },  "separator": { "dash": [6, 6], "dashScale": "absolute" } },
    { "id": "product",   "label": "Product (+rental)",    "from": 0.4,   "to": 0.7,   "style": { "fillOpacity": 0.65 }, "separator": { "dash": [6, 6], "dashScale": "absolute" } },
    { "id": "commodity", "label": "Commodity (+utility)", "from": 0.7,   "to": 1,     "style": { "fillOpacity": 0.8 },  "separator": { "dash": [6, 6], "dashScale": "absolute" } } ] },
"visibility": { "kind": "linear", "label": "Value chain", "min": 0, "max": 1, "reversed": true,
  "endLabels": { "min": "Visible", "max": "Invisible" } }

A Wardley map divides evolution into its four unequal stages and labels the ends of the value chain.

5.4 Numeric axes (linear, log)

Property Type Description
unit string "px" (default), "pt", "mm", "cm", "in", "m", "unitless", or any custom unit label.
scale number Canvas units per domain unit at zoom 1. Default: 1 for px; 96/25.4 for mm (CSS reference pixel); 96 for in; 72/… for pt etc.
precision int Decimal places stored (overrides persistence default for this axis).
base number For log axes (default 10). Domain values MUST be > 0.
format string Number format pattern for rulers (ICU/Unicode LDML, for example "#,##0.0 'mm'").
{
  "mm": { "kind": "linear", "unit": "mm", "precision": 1, "label": "Millimetres",
        "doc": "Real-world length on the printed sheet. 1 mm = 3.78 px at 100 %." }
}

5.5 Time axes (time)

Time axes place elements by date or timestamp. They are used for Gantt charts, roadmaps, timelines, event storms with a real time dimension, and schedules.

Property Type Description
valueType "date", "datetime", "epoch-ms", "epoch-s", "yearMonth" Stored representation of positions on this axis. Default "datetime". yearMonth places by month index (below).
timezone IANA name, "UTC", "floating" Zone in which dates are interpreted and calendar snapping is computed. "floating" means local wall-clock time without zone. Default "UTC". A bindable form { "attribute": "timezone" } refers to a diagram attribute.
scale {unit, size} How much canvas distance one unit of time occupies at zoom 1, for example { "unit": "day", "size": 40 } (40 canvas units per day). Units ($defs/TimeUnit): millisecond, second, minute, hour, day, week, month, quarter, year, decade, century, millennium. For month and larger units the scale is the average length (a month is 30.436875 days). unit MAY be bound to a diagram attribute, { "attribute": "unit" } (below).
calendar Calendar Working time definition (below).
collapse "none", "non-working" If "non-working", non-working time takes no canvas space (a compressed timeline in which weekends vanish). Default "none".
min, max timestamp or {cel} Bounds; may be bound to diagram attributes such as project start and end.
origin timestamp or {cel} Timestamp at canvas x = 0. Default: min if set, else the Unix epoch.
zoomLevels ZoomLevel[] Presets that change scale and the header (for example days, weeks, months).
today object { "visible": true, "style": StyleRef, "label": "Today" } — a marker line at env.now.

Calendar

{
  "calendar": {
    "id": "project",
    "workingDays": [1, 2, 3, 4, 5],
    "workingHours": [["09:00", "12:30"], ["13:30", "17:30"]],
    "holidays": { "attribute": "holidays" },
    "firstDayOfWeek": 1,
    "doc": "Mon–Fri, 8 working hours; holidays come from the diagram's holiday list."
  }
}
Property Type Description
id identifier Name used by CEL time functions (workingDays(a, b, 'project')).
workingDays int[] ISO weekday numbers (1 = Monday … 7 = Sunday). Default [1,2,3,4,5].
workingHours [start, end][] Working intervals per working day. Default [["00:00","24:00"]].
holidays date[] or Bindable Non-working dates.
exceptions {date, workingHours}[] Special days (half days, working Saturdays).
firstDayOfWeek int For week snapping and headers. Default 1 (ISO).

The grid and header of time axes show non-working time shaded by default; the style is set in ruler.nonWorkingStyle.

Units coarser than a year. decade, century and millennium are accepted wherever a time unit is (scale.unit, zoomLevels, ruler levels and ticks, calendar snap rules, grid spacing, and the CEL functions startOf, endOf and addUnits). Their boundaries are the years divisible by 10, 100 and 1000 in astronomical numbering, so year 0 starts a decade, a century and a millennium.

Month axes. With valueType: "yearMonth", the axis domain is the month index of the yearMonth primitive (4.2): origin, min and max are yearMonth strings, positions are stored as yearMonth values, and the scale's unit is month or coarser. timezone, calendar and collapse do not apply, and a validator MUST reject units finer than month on such an axis. In its formats, LDML u MUST be read as the signed astronomical year; a validator MUST reject y, whose era is ambiguous before year 1.

Bound unit. scale.unit and the unit of a calendar snap rule (5.10) MAY be { "attribute": name }, naming a diagram attribute whose values are time units (an enum whose keys or stored forms are unit names, 4.5). Changing the attribute is a model edit that rescales the axis.

"time": { "kind": "time", "valueType": "yearMonth", "origin": "1900-01",
  "scale": { "unit": { "attribute": "unit" }, "size": 4 },
  "ruler": { "visible": true, "position": "bottom",
             "levels": [ { "unit": "decade", "format": "u", "minSpacingPx": 64 } ] } }

A hype cycle's time axis runs over months from any year, before or after the common era, at four canvas units per step of the unit the diagram chooses.

5.6 Ordinal axes (ordinal) and angular axes

Ordinal axes place elements into discrete bands (categories): lanes, rows, resources, lifelines, columns of a Kanban board, days of a timetable.

Property Type Description
categories Category[] or {cel} or {nodes} The bands. Static list, a CEL expression returning a list of {id, label} maps, or { "nodes": "Resource", "orderBy": "self.order" } — one band per node of a type. With nodes, the bands are model elements: they can be edited, reordered and deleted, their node notation is used to draw the band header, their placement is determined by the axis (not by placement), and attributes bound to this axis store a reference to the band node.
bandSize number or "auto" Canvas size of each band. "auto" grows each band to fit its content.
minBandSize, maxBandSize number Limits for auto sizing.
bandSizes {attribute} Per-band size stored on the category element.
padding number Space before the first and after the last band.
gap number Space between bands.
reorderable bool Users may drag bands to reorder them (updates the order attribute or category list).
allowUnassigned bool Whether elements may lie outside any band. Default false.
header object { "size": 120, "label": {cel}, "style": StyleRef } — band headers (the "lane titles").

A Category is { "id": "backlog", "label": "Backlog", "doc": "...", "color": "#…", "size": 200 }.

Position values on an ordinal axis are band references: either the band id as a string, or { "band": "backlog", "offset": 12.5 } where offset is the canvas distance from the band's start edge. The offset lets several elements share a band at different positions (for example activities within a swimlane).

Angular axes (angular) are used with polar coordinate systems (5.7). Properties: unit ("deg" default or "rad"), start (angle of domain 0, default −90 = pointing up), direction ("clockwise" default or "counterclockwise"), wrap (default true, values modulo 360°).

5.7 Coordinate systems

Property Type Description
kind "cartesian", "polar" Required.
x, y Axis or name For cartesian systems.
angle, radius Axis or name For polar systems; radius is linear or ordinal (concentric rings).
center [x, y] Canvas point of the polar origin.
orientation "y-down", "y-up" Cartesian direction of the positive y axis on screen. Default "y-down" (screen convention); engineering drawings often use "y-up".
bounds {x: [min,max], y: [min,max]} Canvas bounds (in domain values); elements must stay inside.
infinite bool Canvas extends indefinitely. Default true unless bounds is set.
grid GridDisplay Visible grid (5.13). Independent of snapping, but defaults to the snapping grid.
snapping Snapping or profile name Default snapping in this system (5.9).
guides Guides Alignment and distribution guides (5.12).
nested map TypeRef → system name Coordinate systems local to containers of a type (below).
label, doc

Nested coordinate systems. A container node may establish its own coordinate system for its children. The children's positions are then stored relative to the container in the nested system's units. Examples: a Kanban board node whose x axis is the ordinal status column set; a sub-timeline inside a project phase; a pin layout inside a chip outline measured in millimetres.

{
  "systems": {
    "board": {
      "kind": "cartesian",
      "x": { "kind": "ordinal", "categories": { "nodes": "Column", "orderBy": "self.position" }, "bandSize": 260, "gap": 16 },
      "y": { "kind": "linear", "unit": "px" },
      "nested": { "Swimlane": "laneLocal" }
    }
  }
}

A position in a nested system is converted to canvas coordinates through the container's placement. Frames compose: a node inside a lane inside a pool resolves its canvas position through all three.

5.8 Placement

Placement tells the runtime, for each node type, where each coordinate of an element comes from. It is declared in the node notation (6.9) under placement, because the same model type may be placed differently in different viewpoints.

Property Type Description
system name Coordinate system. Default: the viewpoint's system, or the nearest container's nested system.
x, y PlacementSource Position of the element's anchor on each axis.
x2, y2 PlacementSource Position of the opposite edge; if given, the extent is x2 − x in domain units (for example end − start on a time axis).
width, height PlacementSource Extent in domain units (a duration on time axes, a number of bands on ordinal axes). Mutually exclusive with x2/y2.
angle, radius PlacementSource For polar systems.
anchor Anchor Which point of the node the position refers to: "top-left" (default), "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right", or [fx, fy] fractions. Point-like elements on time axes (milestones) typically use "center".
movable bool or {x, y} Whether users may move the element along each axis. Default true. { "x": true, "y": false } constrains dragging to the x direction.
resizable bool or {x, y} Whether users may change the extent along each axis (see also size in 6.9).
stack Stack How elements that share a band are arranged (below).
clampToParent bool Keep the element within its container's content area. Default true for contained nodes.
doc Doc Explains placement to users ("Drag horizontally to reschedule; drag vertically to reassign").

PlacementSource is one of:

Form Kind Meaning
"free" (or absent) free The value is stored in view data (11.6); moving the element updates the view only.
{ "attribute": "start" } bound The value is read from and written to a model attribute. Moving the element changes the model (and triggers change hooks and constraints).
{ "attribute": "start", "offset": "P1D" } bound Bound with a constant offset in domain units.
{ "cel": "…" } computed Read-only; the element cannot be moved along this axis.
{ "cel": "…", "write": { "set": { "attr": "value - duration('1h')" } } } computed with inverse The expression computes the position; when the user moves the element, the write actions (an Action, 9.4) run with value bound to the new snapped domain value. This allows bindings such as "x is start, width is durationDays * 1 day".
{ "layout": true } layout Determined by the layout algorithm (section 10); not user-movable.

The attribute type MUST be compatible with the axis: number/int for numeric axes, date/datetime for time axes, string, enum or a reference to a category node type for ordinal axes, number for angular axes.

Stacking — when several elements fall into the same ordinal band and overlap along the other axis (two tasks of one resource in the same week), stack decides what happens:

Property Type Description
mode "overlap", "stack", "pack" overlap: draw over each other; stack: every element gets its own sub-row in order; pack: greedy interval packing into the fewest sub-rows. Default "overlap".
order Expression Sort key for stacking (for example "self.start").
rowSize number Sub-row height.
growBand bool Band grows to fit sub-rows (requires bandSize: "auto").

Example: a Gantt task

{
  "placement": {
    "system": "schedule",
    "x":  { "attribute": "start" },
    "x2": { "attribute": "end" },
    "y":  { "attribute": "assignee" },
    "movable": { "x": true, "y": true },
    "resizable": { "x": true, "y": false },
    "stack": { "mode": "pack", "order": "self.start", "rowSize": 28, "growBand": true },
    "doc": "Drag to reschedule or reassign. Drag the right edge to change the end date."
  }
}

Dragging the task horizontally moves both start and end (keeping the duration); dragging its right edge changes end only; dragging it into another lane changes assignee to that lane's category id (or node reference).

5.9 Snapping: model

Snapping restricts where elements can be placed and how they can be sized, so that diagrams stay tidy and values stay meaningful (a task starts on a day, not at 13:47:12; a component sits on a 2.54 mm pitch).

Snapping is defined per axis. Each axis of a coordinate system can have its own rule, so a diagram type may snap only horizontally, only vertically, or both, with different rules on each. The same rule model applies to positions, sizes, edge bendpoints, label offsets and rotation.

{
  "snapping": {
    "enabled": true,
    "mode": "always",
    "x": { "calendar": { "unit": "day", "align": "nearest" } },
    "y": { "bands": { "align": "center" } },
    "size": { "x": { "calendar": { "unit": "day" }, "min": "P1D" } },
    "bendpoints": "inherit",
    "targets": ["rule", "objects", "guides"],
    "toleranceScreenPx": 8,
    "bypassModifier": "Alt",
    "feedback": { "showGhost": true, "showValue": true },
    "doc": {
      "summary": "Tasks snap to whole days and into resource lanes.",
      "rationale": "Plans are made in days; sub-day precision would suggest accuracy the plan does not have."
    }
  }
}
Property Type Description
enabled bool Master switch; users MAY toggle it at run time if userToggle is true. Default true when snapping is present.
userToggle bool Users may switch snapping on and off (toolbar button, shortcut). Default true.
mode "always", "magnetic" always: the value is always replaced by the nearest allowed value; magnetic: only when within toleranceScreenPx of an allowed value (free placement otherwise). Default "always" for rule snapping, "magnetic" for object and guide snapping.
x, y SnapRule Rule for positions along each axis (5.10). Absent or "none" = continuous.
both SnapRule Shorthand applying the same rule to x and y (numeric axes only).
angle, radius SnapRule For polar systems.
size {x, y, both} of SnapRule Rules for extents (width/height). Default: follows the position rule of the axis for bound extents, none for free ones.
reference "anchor", "bounds", "center", "edges" Which point(s) of the element are snapped: the placement anchor (default), any edge of the bounds (the nearest wins), the center, or leading edges only.
rotation {step, tolerance} Rotation snapping in degrees (for example { "step": 15 }).
bendpoints "inherit", "none" or {x, y} Snapping for edge bendpoints and orthogonal segments.
labels "none" or LabelSnap Snapping for draggable labels (for example to the nearest of start, middle, end on an edge).
targets string[] Which snapping sources participate, in priority order: "rule" (the axis rules), "objects" (edges and centers of other elements), "guides" (user guides), "ports", "parent" (container edges and padding). Default ["rule"].
toleranceScreenPx number Attraction distance for magnetic snapping and object snapping. Default 8.
applyToProgrammatic bool Whether changes made by operations, hooks and templates are snapped too. Default false.
bypassModifier "Alt", "Shift", "Ctrl", "Meta", "none" Holding this key temporarily disables snapping. Default "Alt".
feedback object showGhost (preview of snapped position), showValue (tooltip with the snapped domain value, formatted by the axis), highlightBand (for ordinal axes), style (StyleRef for snap indicator lines).
byGesture map gesture → Snapping Snapping for one kind of gesture, merged over this level (below). Keys: move, resize, drop, paste, handle.
doc Doc Explains the snapping to users; runtimes SHOULD show summary when snapping is toggled or while dragging.

Where snapping is declared, and precedence. Snapping can be declared in a snap profile, on a coordinate system, on a viewpoint's canvas, on a node or edge notation (snapping property, 6.9 and 6.10), and on a placement. The most specific declaration wins per property and per axis: a node notation that only declares x keeps the system's y rule. "inherit" explicitly refers to the next level.

Snapping per gesture. An entry of byGesture applies to that gesture only and is more specific than the level it is declared on: it MUST be merged over that level per property and per axis, with the rule above. move covers dragging an element, resize dragging its extent, drop a toolbox drop, a create at the pointer (7.2) and a context-menu entry run at a point (7.3), paste pasting, and handle dragging a shape handle (6.8). reference MAY differ per gesture.

(DISL 0.3) A drop (and a create at the pointer or a context-menu add) of a new element uses the snapping of the node notation of the type it creates, merged over its coordinate system's, and the value a rule receives is the position, with the new element's placement anchor at the pointer, of the point reference selects (the placement anchor itself by default).

"snapping": {
  "x": { "grid": { "spacing": 1 }, "ties": "away-from-zero" },
  "y": { "grid": { "spacing": 1 }, "ties": "away-from-zero" },
  "byGesture": { "drop": { "x": { "grid": { "spacing": 1 }, "direction": "floor" }, "reference": "center" } }
}

On a hype cycle a move or resize snaps to the nearest step, while a toolbox drop lands in the step that contains the drop point and finds its row from the element's middle.

5.10 Snap rules

A SnapRule is "none", a profile name, or an object with exactly one of the rule keys below, plus optional common properties.

Rule key Applies to Description
grid numeric axes Regular grid: { "spacing": 10, "offset": 0, "subdivisions": 2 }. Positions snap to offset + k·spacing (or to subdivisions when zoomed in beyond subdivideAtZoom).
values numeric, time, angular Explicit list of allowed values: { "values": [0, 12.5, 25, 50, 100] }. May be {cel} returning a list.
calendar time Calendar units: { "unit": "day", "step": 1, "align": "nearest", "workingTime": true, "calendar": "project", "at": "09:00" } (details below).
ticks any Snap to the axis ticks visible at the current zoom level: { "ticks": { "level": "minor" } }. Useful for zoom-adaptive snapping.
bands ordinal Snap into bands: { "align": "start" | "center" | "end" | "keep-offset", "offsetGrid": 8 }. keep-offset preserves the offset within the band but still snaps offsetGrid if given.
divisions numeric extents, angular Divide a range into n equal parts: { "count": 12, "of": "parent" } — positions at twelfths of the container (column layouts).
ratio sizes Keep the aspect ratio or snap to preferred ratios: { "ratios": [1, 1.5, 2] }.
cel any Custom function: { "cel": "value < 100.0 ? snap(value, 5.0) : snap(value, 25.0)" }. Receives value (domain value), axis, zoom, self, parent; returns the snapped domain value.
byZoom any Zoom-dependent rules: { "byZoom": [ { "maxZoom": 0.5, "rule": { "grid": { "spacing": 50 } } }, { "rule": { "grid": { "spacing": 10 } } } ] } — the first entry whose maxZoom ≥ current zoom applies; the last entry without maxZoom is the fallback.
plugin any { "plugin": "acme.snap-to-rails", "args": {…} }.

Common properties of snap rules are written next to the rule key, not inside it ({ "grid": { "spacing": 10 }, "min": 20 }):

Property Type Description
min, max domain value Clamp the snapped value (for sizes: minimum and maximum extent).
direction "nearest", "floor", "ceil" Rounding direction. Default "nearest".
ties "away-from-zero", "toward-zero", "up", "down", "even" How a value exactly halfway between two allowed values rounds when direction is nearest. Default "away-from-zero".
mode "always", "magnetic" Overrides the snapping-level mode for this rule.
doc Doc Explanation for this axis.

Ties. With direction: "nearest", a value exactly halfway between two allowed values MUST round as ties says: away-from-zero to the one farther from zero (on both sides of the origin, as CEL math.round does), toward-zero to the one nearer zero, up to the greater, down to the smaller, even to the one that is an even multiple of the step from the rule's offset. The default makes declared snapping and CEL snapping (snap, 12.4) agree.

Calendar rules snap timestamps using the axis's time zone and calendar:

Property Type Description
unit millisecond … millennium, or {attribute} Snapping unit ($defs/TimeUnit, 5.5). week respects firstDayOfWeek; month and coarser units snap to calendar boundaries (not to fixed durations). May be bound to a diagram attribute, as scale.unit (5.5).
step int Multiples of the unit (for example 15 minutes: { "unit": "minute", "step": 15 }). Steps are counted from the start of the next larger unit (15-minute steps restart every hour).
align "start", "end", "nearest" Snap to the start of the unit, its end, or whichever boundary is closer. Default "nearest".
workingTime bool Only working time of calendar is allowed; values in non-working time move to the nearest working boundary (in direction).
calendar id Calendar to use; default: the axis calendar.
at time Time of day for day-level snapping (tasks start at 09:00, not midnight).

Size snapping on time axes uses durations: { "calendar": { "unit": "day" }, "min": "P1D" } makes every task at least one day long and a whole number of days.

Applying rules on an axis — algorithm. When the user drags an element (or a handle, bendpoint or label), for each axis independently:

  1. Convert the pointer's canvas position to the axis's domain value through the axis scale (and the container's nested frame, if any).
  2. If snapping is disabled (globally, by the bypass modifier, or for this axis), use the raw value.
  3. Compute candidates: the rule result (if "rule" is a target), the nearest object/guide/port/parent alignment candidates within tolerance.
  4. Choose the highest-priority candidate in targets order that exists; for magnetic mode, discard candidates farther than the tolerance (measured in screen pixels after mapping back).
  5. Clamp to rule min/max, axis min/max and container bounds.
  6. For bound placements, write the domain value to the attribute; for free placements, to the view data.

The rule result MUST be idempotent: snapping an already snapped value returns the same value. Runtimes MUST apply snapping to programmatic changes made through tools and operations only if the specification sets snapping.applyToProgrammatic: true (default false); values typed into forms are never snapped, but a placement constraint (8.4) can reject them.

5.11 Snapping examples

Grid in both directions (classic node-and-edge diagram):

{
  "snapProfiles": {
    "grid10": {
      "both": { "grid": { "spacing": 10 } },
      "size": { "both": { "grid": { "spacing": 10 }, "min": 20 } },
      "doc": "Everything aligns to a 10 px grid."
    }
  }
}

Horizontal only (a timeline of freely stacked cards):

{
  "snapping": { "x": { "calendar": { "unit": "week", "align": "start" } }, "y": "none" }
}

Vertical only (a sequence diagram: lifelines are placed by layout on an ordinal x axis, messages snap to 20 px rows):

{
  "snapping": { "x": { "bands": { "align": "center" } }, "y": { "grid": { "spacing": 20, "offset": 60 } } }
}

Different rules per axis and zoom (a floor plan in millimetres):

{
  "snapping": {
    "x": { "byZoom": [ { "maxZoom": 0.25, "rule": { "grid": { "spacing": 500 } } },
                       { "maxZoom": 1.0,  "rule": { "grid": { "spacing": 100 } } },
                       { "rule": { "grid": { "spacing": 10 } } } ] },
    "y": "inherit-x",
    "rotation": { "step": 90 },
    "doc": "Walls snap to 10 cm, 50 cm or 1 cm depending on zoom; rotation in right angles."
  }
}

"inherit-x" / "inherit-y" copy the rule of the other axis, a convenience when rules are long.

Working-time snapping (15-minute slots during office hours):

{
  "x": { "calendar": { "unit": "minute", "step": 15, "workingTime": true, "calendar": "office" } }
}

Circuit pins on a 2.54 mm pitch with magnetic port snapping:

{
  "snapping": { "both": { "grid": { "spacing": 2.54 } }, "targets": ["ports", "rule"], "toleranceScreenPx": 10 }
}

5.12 Guides

Property Type Description
smart bool Show alignment guides to edges and centers of nearby elements while dragging. Default true.
distribution bool Show equal-spacing guides. Default true.
user bool Users may drag guides out of rulers; guides are stored in view data. Default false.
style StyleRef Style of guide lines.

5.13 Grid display and rulers

The visible grid is separate from snapping (a grid can be displayed without snapping and vice versa) but defaults to mirroring the snapping rules.

Property Type Description
visible bool Default false.
style "lines", "dots", "crosses", "none" Default "lines".
spacing number or {x, y} Canvas spacing; for time axes, a calendar unit { "unit": "day" }. Default: from snapping.
majorEvery int or {x, y} Every n-th line is major.
minorColor, majorColor Paint Colors (tokens recommended).
minZoomForMinor number Hide minor lines below this zoom.
bands object For ordinal axes: { "alternate": true, "fill": [Paint, Paint], "separator": Stroke } — zebra striping of lanes.
nonWorking Style For time axes: fill for non-working time.

A Ruler (axis property ruler) configures rulers and headers: visible, position ("top", "bottom", "left", "right"), size, and levels — a list of header rows for multi-level time scales:

{
  "ruler": {
    "position": "top",
    "levels": [
      { "unit": "month", "format": "MMMM yyyy" },
      { "unit": "week",  "format": "'W'w",  "minZoom": 0.5 },
      { "unit": "day",   "format": "EEE d", "minZoom": 1.0 }
    ]
  }
}

Formats use Unicode LDML date patterns for time axes and number patterns for numeric axes; ordinal axes show category labels.

A ruler level also accepts minSpacingPx, which shows the level only while its adjacent labels are at least that many screen pixels apart (an alternative to minZoom and maxZoom), and step, a multiple of unit (default 1). A Ruler gains:

Property Type Default Description
ticks {minSpacingPx, steps: [{unit, step, format}]} ($defs/RulerTicks) – A single adaptive row: of steps, listed from finest to coarsest, the finest whose labels are at least minSpacingPx apart on screen is used. ticks and levels MUST NOT both be set.
boundaryFormats map unit → format – A tick that falls exactly on the start of a listed unit uses that unit's format; when several apply, the coarsest unit wins.
attach "view", "canvas" "view" view: the ruler stays at its edge of the pane whatever is scrolled; canvas: it scrolls with the drawing.
minUnit TimeUnit or {attribute} – (DISL 0.3) The shortest step shown: a level, or a step of ticks, whose step is shorter than minUnit MUST NOT be shown, at any zoom. {attribute} binds it to an attribute of the diagram, such as the unit the diagram is drawn in.

Ticks and level boundaries MUST fall on round boundaries of their unit in the axis's time zone, never on offsets from the edge of the viewport; on a yearMonth axis, on month indices divisible by the step's number of months, counted from year 0.

"ruler": { "visible": true, "position": "bottom", "size": 24,
  "ticks": { "minSpacingPx": 80, "steps": [
    { "unit": "hour", "step": 1, "format": "HH:mm" }, { "unit": "hour", "step": 6, "format": "HH:mm" },
    { "unit": "day", "step": 1, "format": "MMM d" }, { "unit": "week", "step": 1, "format": "MMM d" },
    { "unit": "month", "step": 1, "format": "MMM yyyy" }, { "unit": "year", "step": 1, "format": "yyyy" } ] },
  "boundaryFormats": { "month": "MMM yyyy", "year": "yyyy" } }

A timeline's one ruler row chooses the finest step that leaves 80 pixels per label, and writes the bare year on the first of January.

"ruler": { "visible": true, "position": "bottom", "minUnit": { "attribute": "unit" },
  "levels": [ { "unit": "month", "format": "MMM u", "minSpacingPx": 64 }, { "unit": "year", "format": "u", "minSpacingPx": 64 },
              { "unit": "decade", "format": "u", "minSpacingPx": 64 } ] }

A hype cycle drawn in decades starts its ruler at decades, and one drawn in months at months: a diagram in years never shows the month level, however far it is zoomed in.

Illustrated by

Referenced from