Using reladraw with Org Mode

· back

monkey-id
Using reladraw with Org Mode

Reladraw (github) is a new declarative diagramming package that allows manually specify node position (relative). With Graphviz it's always a pita with specifying invisible links to achieve similar.

So on <2026-10-06 Tue> I asked CC to build ob-reladraw.el so I can use from Org Mode.

Let's try some examples.

Examples

Architecture

architecture
// Transcribed from a draw.io diagram.

// --- Styles -----------------------------------------------------------------
style synced     fill: theme-primary-subtle  border: theme-primary  line: theme-primary
style dim        text: (color: theme-muted)
style backup     border: #d2904e  line: #d2904e
style dump       fill: theme-secondary-subtle  border: theme-border  shape: document
style invisible  fill: none  border: none


// --- The hub and its four machines ------------------------------------------
// The cluster is wrapped in an invisible container so things outside it can be
// placed against the whole cluster rather than against the hub alone.

node cluster ""  style: invisible

node cluster.hub "Cloud sync"  badge: package  style: synced

node cluster.desktop1       "Desktop 1 / Linux"    above-left of cluster.hub   gap: tight  badge: desktop
node cluster.desktop1.files "shared folder / [dim]synced[/dim]"  style: synced

node cluster.desktop2       "Desktop 2 / Windows"  above-right of cluster.hub  gap: tight  badge: desktop
node cluster.desktop2.files "shared folder / [dim]synced[/dim]"  style: synced

node cluster.laptop1        "Laptop 1 / macOS"     below-left of cluster.hub   gap: tight  badge: laptop
node cluster.laptop1.files  "shared folder / [dim]synced[/dim]"  style: synced

node cluster.laptop2        "Laptop 2 / macOS"     below-right of cluster.hub  gap: tight  badge: laptop
node cluster.laptop2.files  "shared folder / [dim]synced[/dim]"  style: synced

edge cluster.desktop1.files <-> cluster.hub  style: synced  from: right  to: top
edge cluster.desktop2.files <-> cluster.hub  style: synced  from: left   to: top
edge cluster.laptop1.files  <-> cluster.hub  style: synced  from: right  to: bottom
edge cluster.laptop2.files  <-> cluster.hub  style: synced  from: left   to: bottom


// --- The database dumps -----------------------------------------------------
// Nothing says how wide the corridor between the server and the machines is.
// Putting the dumps in between is what pushes those two apart.

node dumps ""  right of server.docker  left of cluster (gap: wide)  level with server.docker  style: invisible
node dumps.db1 "db dump / (app 1)"  style: dump
node dumps.db2 "db dump / (app 2)"  style: dump

node dumps_note "Written by nightly cron job." (wrap: 30)  below dumps  gap: tight  shape: none


// --- The server -------------------------------------------------------------

node server         "Home Server"  left of cluster  badge: desktop  contents: (widths: match)
node server.mirror  "shared folder / mirror"  style: backup
node server.deploy  "deploy dir"  below server.mirror  gap: normal

// The cubes are nodes rather than ornament: two of them are where the dump
// arrows start. The lower pair is centered under the upper three by naming two
// targets and nothing else.
node server.docker  "Docker" (at: bottom-center, align: center)  below server.deploy  gap: normal
node server.docker.a  icon: cube
node server.docker.b  right of server.docker.a  gap: tight  icon: cube
node server.docker.db1  right of server.docker.b  gap: tight  icon: cube
node server.docker.c  below server.docker.a and server.docker.b    gap: tight  icon: cube
node server.docker.db2  below server.docker.b and server.docker.db1  gap: tight  icon: cube

edge server.deploy -> server.docker  "compose up"

edge server.docker.db1 -> dumps.db1  from: right  to: left
edge server.docker.db2 -> dumps.db2  from: right  to: left

// The rclone lines run up the channel between the two left-hand machines, so
// the dumps arrive through the middle of the cluster rather than across one.
edge dumps.db1 -> cluster.hub  "rclone"  between cluster.desktop1 and cluster.laptop1  from: right  to: left
edge dumps.db2 -> cluster.hub  "rclone"  between cluster.desktop1 and cluster.laptop1  from: right  to: left

edge cluster.desktop1.files -> server.mirror  "backup script"  style: backup  from: left  to: right
edge cluster.desktop1.files -> server.deploy  "deploy script"                 from: left  to: right


// --- The backup drive -------------------------------------------------------

node drive          "External HD"  left of server  top level with server  badge: disk  contents: (widths: match)
node drive.mirror   "shared folder / backup"  style: backup
node drive.archive  "archive"

edge server.mirror -> drive.mirror  style: backup  from: left  to: right

node drive_note "Swapped with a second drive weekly." (wrap: 22)  left of drive.mirror  gap: tight  shape: none

example-architecture__reladraw.svg

Beside a group

A node placed against several others at once.

beside a group
// Naming several targets places a node against the region that bounds them all.

node one    "first"
node two    "second"                 below one
node three  "third with a longer label"  below two

// A direction clears the furthest of them. This one says nothing about its own
// vertical position — that comes from the region.
node spans  "level with all three, and clear of the widest" (wrap: 24)  right of one and three  gap: wide  shape: none

// An alignment uses the region as a position. "top" is its top edge, not its middle.
node tops   "top level with the second box" (wrap: 20)                  right of two  top level with two  gap: tight  shape: none

node beside  "centered on the top two"  right of spans  level with one and two  gap: wide
example-beside-a-group__reladraw.svg

Gap sizes

Tight and wide gaps, and which one wins when both ends ask.

gap sizes
// Gaps: on a placement, on a node, and what happens when both ends ask.

style plain  fill: theme-fill  border: theme-border

node left_wall  "Left wall"   style: plain
node right_wall "Right wall"  right of left_wall  style: plain

// Both sides bind and they want different distances, so each placement carries
// its own gap.
node wedge "Wedged tight left, wide right"  right of left_wall (gap: tight)  left of right_wall (gap: wide)  level with left_wall  style: plain

// `gap:` on the node is the default for whatever the placements do not say.
node stack "Node default is wide"  below wedge  right of left_wall (gap: tight)  gap: wide  style: plain

// `none` is contact, and the other placement still keeps its distance.
node shelf "Flat on the stack"  below stack (gap: none)  right of left_wall (gap: tight)  style: plain

edge left_wall -> wedge  from: right  to: left
edge wedge -> right_wall  from: right  to: left

// A node's gap also reaches placements written against it. `island` names nobody
// and asks for wide, so both neighbors are held off at that distance. `west`
// asks for tight and gets wide: a gap is a minimum, so the larger one holds.
node island "States wide, names nobody"  below shelf  gap: wide  style: plain
node east   "Takes the island's wide"    right of island  level with island  style: plain
node west   "Asks tight, gets wide"      left of island   level with island  gap: tight  style: plain
example-gap-sizes__reladraw.svg

As close as allowed

A node sits as close to what it names as its placements allow.

as close as allowed
// A node sits as close to what it names as its placements allow.

node anchor "anchor"
node far    "far"   right of anchor
node near   "near"  right of far

node trailing "one tight gap from near"  left of near  below anchor  gap: tight  overlap: allow  shape: none
example-as-close-as-allowed__reladraw.svg

No overlaps

Nodes never overlap, even where nothing says which way to move.

no overlaps
// Nodes never overlap. Nothing here says the wide node has to clear "left".
//
// Which way it moves is read off the file: "middle" is right of "left", so
// rightward is the way that contradicts nothing written. And "left" is tall, so
// the two overlap far less horizontally than vertically.

node left    "L/L/L/L/L/L/L/L/L/L/L/L/L/L"
node right   "R"                             right of left
node middle  "m"   right of left  left of right  level with left
node wide    "a wide box that overruns both"    below middle  gap: tight
example-no-overlaps__reladraw.svg

Inside a container

How children line up in a container wider than they are.

inside a container
// How a container's contents sit inside it, when the container is wider than
// they are. A container is as wide as the wider of its title and its contents,
// so a long title leaves slack — and every member sits at the smallest position
// its constraints allow, which puts all of that slack on the right.
//
// `contents: (widths: …, align: …)`. The two are independent: equal widths
// ranged left is a real arrangement, and so is natural widths centered.

style plain  fill: theme-fill  border: theme-border

// Nothing said. Natural widths, and the column sits against the left.
node ragged "Ingestion and enrichment pipeline" (at: top-center, align: center)
node ragged.one   "parse"  style: plain
node ragged.two   "resolve and lay out"  style: plain
node ragged.three "draw"  style: plain

// One edge down each side of the column, still against the left of the slack.
node block "Ingestion and enrichment pipeline" (at: top-center, align: center)  right of ragged  contents: (widths: match)
node block.one   "parse"  style: plain
node block.two   "resolve and lay out"  style: plain
node block.three "draw"  style: plain

// The same column, moved into the middle of the room the title asked for.
node middle "Ingestion and enrichment pipeline" (at: top-center, align: center)  right of block  contents: (widths: match, align: center)
node middle.one   "parse"  style: plain
node middle.two   "resolve and lay out"  style: plain
node middle.three "draw"  style: plain

// `fill` takes the whole band, so there is no slack left for `align` to say
// anything about. Under a title this long that is what `match` could not do.
node full "Ingestion and enrichment pipeline" (at: top-center, align: center)  right of middle  contents: (widths: fill)
node full.one   "parse"  style: plain
node full.two   "resolve and lay out"  style: plain
node full.three "draw"  style: plain

// Natural widths, centered: the column keeps its ragged edges and the block of
// them sits in the middle. The cell of the cross product that needs both words.
node loose "Ingestion and enrichment pipeline" (at: top-center, align: center)  below ragged  contents: (align: center)
node loose.one   "parse"  style: plain
node loose.two   "resolve and lay out"  style: plain
node loose.three "draw"  style: plain

// A style may carry it, which `align: widths` never could — it read only what
// the node itself wrote, so a style carrying it did nothing and said nothing.
style boxed  contents: (widths: match, align: center)

node carried "Ingestion and enrichment pipeline" (at: top-center, align: center)  right of loose  style: boxed
node carried.one   "parse"  style: plain
node carried.two   "resolve and lay out"  style: plain
node carried.three "draw"  style: plain
example-inside-a-container__reladraw.svg

Shapes

The outlines a node can be drawn with, and the pictures it can be drawn as.

shapes
// What a node is drawn as. `shape:` is the outline it is drawn with; `icon:` is
// a picture it is drawn as, with no box at all.

style artifact  fill: theme-secondary-subtle  border: theme-border  shape: document

// The fold is a fixed size rather than a fraction of the box, so it looks the
// same on all three of these.
node narrow "Report"  style: artifact
node medium "Nightly database dump"        right of narrow  gap: tight  style: artifact
node wide   "A very much longer file name than either of the other two"  right of medium  gap: tight  style: artifact

// A picture has no outline, no fill and no padding. The text goes underneath.
node services ""  below narrow  gap: normal  fill: none  border: none
node services.web    "web"                          icon: cube
node services.api    "api"     right of services.web  gap: tight  icon: cube
node services.worker "worker"  right of services.api  gap: tight  icon: cube

// No text at all: a picture usually is the statement, so it gets no caption.
node bare1  below services (gap: normal)  icon: cube
node bare2  below bare1     gap: tight  icon: cube

// A picture body is an ordinary node, so it takes edges and sides like any
// other. That is what it buys over a badge, which cannot be the end of an arrow.
node sink "collector"  right of bare1 and bare2 (gap: wide)  level with bare1, bare2
edge bare1 -> sink  from: right  to: left
edge bare2 -> sink  from: right  to: left

// Any icon works as a body, not just the cube.
node store  below bare2 (gap: normal)  icon: database
node drive  right of store  gap: tight  icon: disk

// `at: bottom-center` puts the text under the contents, and `align: center`
// ranges its lines against each other; neither implies the other. The lower cube is centered under the upper
// pair by naming both of them and nothing else.
node pool "Worker pool" (at: bottom-center, align: center)  right of store and drive (gap: wide)  level with store, drive
node pool.one  icon: cube
node pool.two  right of pool.one  gap: tight  icon: cube
node pool.three  below pool.one and pool.two  gap: tight  icon: cube
example-shapes__reladraw.svg

Icons

The seven icons, and the ways to use one.

icons
// The seven icons, and the two ways to use one: badge: puts the picture beside
// a node's text, and icon: draws the node as the picture, with no box.

// A style can carry a badge, so every store in a diagram looks alike without
// anyone typing the word twice.
style store  fill: theme-primary-subtle  border: theme-primary  badge: database

// badge: is shorthand. row1.disk below is the same as writing
//   node row1.disk "Spinning disk"
//   node row1.disk.badge  icon: disk  right of row1.disk text
node row1 ""  fill: none  border: none
node row1.disk     "Spinning disk"     badge: disk
node row1.desktop  "Workstation"       right of row1.disk     gap: tight  badge: desktop
node row1.laptop   "Portable"          right of row1.desktop  gap: tight  badge: laptop

node row2 ""  below row1  gap: tight  fill: none  border: none
node row2.package  "Archive"           badge: package
node row2.cubes    "Service instances" right of row2.package  gap: tight  badge: cubes
node row2.db       "Records"           right of row2.cubes    gap: tight  style: store

// icon: the node is the picture itself, and has no text unless you give it one.
node row3 ""  below row2  gap: tight  fill: none  border: none
node row3.cube     icon: cube
node row3.db       icon: database    right of row3.cube     gap: tight
node row3.laptop   "Portable"  icon: laptop  right of row3.db  gap: tight

// No text, so the node is just the badge and its padding.
node bare ""  below row3  gap: tight  badge: disk

// A badge is two lines tall, so it shrinks and grows with the text beside it.
node small "Small text / and a small badge" (size: small)  right of bare   gap: tight  badge: laptop
node large "Large text / and a large badge" (size: large)  right of small  gap: tight  badge: laptop
example-icons__reladraw.svg

Sides and corners

Nodes placed against part of another node: a side, a corner, or its text.

sides and corners
// A placement's target may be a *part* of a node — its text, one of its four
// sides, or one of its nine points — written as a separate word after the
// node's name. Three direction words go with a part:
//
//   inside   wholly within, against that part
//   outside  wholly beyond it
//   on       centered on it, so a node on a corner straddles it
//
// `inside` and `outside` are shorthands whose expansion is derived rather than
// listed: inside is the direction from the named part toward the box's center,
// outside is away from it. `inside right` is `left of`, `inside top-right` is
// `below-left of`, and nobody writes a table.

style mark   shape: none  text: (color: theme-muted)
style spacer  fill: none  border: none
style link  shape: none  text: (color: #6fa8dc)

// All nine points at once. The board is held open by six invisible children
// with nothing in them, which is also the point being made: a box is sized by
// what it *holds*, and the nine marks stamped on it are not held by anything.
node board ""  fill: theme-fill  border: theme-border
node board.nw ""  style: spacer
node board.n  ""  style: spacer  right of board.nw  gap: wide
node board.ne ""  style: spacer  right of board.n   gap: wide
node board.sw ""  style: spacer  below board.nw  gap: wide
node board.s  ""  style: spacer  right of board.sw  below board.n   gap: wide
node board.se ""  style: spacer  right of board.s   below board.ne  gap: wide

node p1 "top-left"       inside board top-left       style: mark
node p2 "top-center"     inside board top-center     style: mark
node p3 "top-right"      inside board top-right      style: mark
node p4 "left-center"    inside board left-center    style: mark
node p5 "center"         on board center             style: mark
node p6 "right-center"   inside board right-center   style: mark
node p7 "bottom-left"    inside board bottom-left    style: mark
node p8 "bottom-center"  inside board bottom-center  style: mark
node p9 "bottom-right"   inside board bottom-right   style: mark

// The case the design record names: an ordinary box with its text in the middle
// and, centered at the bottom inside it, a small link.
node bob "Bob the builder"  below board  gap: normal
node bob.card ""  style: spacer
node bob_link "bob.example.com"  inside bob bottom-center  style: link  url: "https://bob.example.com"

// The inset is a named gap on the placement, and `none` puts the node hard
// against the edge. It is deliberately not the node's own `gap:`, which says
// how this node stands off its neighbours rather than how far in a stamp sits.
node plate "A wide plate to stamp on"  right of bob  gap: wide
node flush "flush"  inside plate top-right (gap: none)  style: mark

// `on` is the thing the old dedicated overlay word could not say: the node's
// center on the point, so it straddles the corner instead of tucking inside.
node dot "A box with a dot on its corner"  below plate  gap: wide
node straddle "*"  on dot top-right  style: mark

// A side is a segment rather than a point, so the axis it does not name is
// left free — this sits against the right edge and centers down it.
node column "A tall box"  right of dot  gap: wide
node column.one "one"
node column.two "two"
node column.three "three"
node column_mark "inside right"  inside column right  style: mark

// `outside` is the same rule read the other way: wholly beyond the named
// part, which is how a caption sits clear of one edge and no other.
node under "outside bottom"  outside column bottom  style: mark

// A part target works on a picture as readily as on a box.
node unit  icon: cube  right of plate  gap: normal
node tag "x3"  inside unit bottom-right  style: mark

// The overlap exemption is for the pair and nothing else: `flush` is still kept
// clear of everything in the drawing that is not `plate`.
edge bob -> plate  from: right  to: left
example-sides-and-corners__reladraw.svg

On the parent's edge

A child placed on its own container's side or corner.

on the parent's edge
// A child may be placed against a part of its own parent — a side, a corner,
// or the parent's text — with the same words used against anything else. The
// parent grows to hold it. Whether the parent has a title band is not stated
// anywhere: there is one when something sits below the text, and not when
// everything sits beside it or against the frame.

style mark  fill: #cdb96f  border: #8f7a3a  text: (color: #111111)

// Four children in the four corners, and nothing below the text, so there is
// no band: the text sits in the middle, as a leaf's does.
node corners "Deployment"
node corners.a "north-west"  inside corners top-left
node corners.b "north-east"  inside corners top-right
node corners.c "south-west"  inside corners bottom-left
node corners.d "south-east"  inside corners bottom-right

// Put two of them below the text and the band is back, made by placement.
node band "Deployment"  right of corners  gap: wide
node band.a "north-west"  below band text  inside band left
node band.b "north-east"  below band text  inside band right
node band.c "south-west"  inside band bottom-left
node band.d "south-east"  inside band bottom-right

// With a corner free there is room in the text's row, and `at:` ranges the
// text across it.
node ranged "Deployment" (at: top-right)  right of band  gap: wide
node ranged.a "north-west"  inside ranged top-left
node ranged.c "south-west"  inside ranged bottom-left
node ranged.d "south-east"  inside ranged bottom-right

// Three children naming the same side are one group: a column in the order
// written, centered on the side together. The ordinary contents keep their
// stack beside it.
node column "Payments service"  below corners  gap: wide
node column.api "api"
node column.db "database"
node column.one "one"      inside column right  style: mark
node column.two "two"      inside column right  style: mark
node column.three "three"  inside column right  style: mark

// A child against the top side sits over the title, and the title and the
// contents make way below it.
node paused "Queue worker"  right of column  gap: wide
node paused.jobs "jobs"
node paused.retry "retries"
node paused.flag "PAUSED"  inside paused top  style: mark

// A side and a row at once: against the right edge, level with one child.
node server "Server"  right of paused  gap: wide
node server.web "web"
node server.db "database"
node server.cache "cache"
node server.hot "hot"  inside server right  level with server.db  style: mark

// Beside the text, the way a badge is. With nothing below the text the two
// center in the box as one group.
node fresh "Release notes"  below column  gap: wide
node fresh.tag "new"  right of fresh text  style: mark

// On a corner rather than inside it: centered on the point, so it straddles
// the edge.
node dot "Inbox"  right of fresh  gap: wide
node dot.count "3"  on dot top-right  style: mark

// Beyond the frame. The note is still part of the router, so the switch
// placed right of the router keeps clear of the note too.
node router "Router"  right of dot  gap: wide
node router.note "note"  outside router right (gap: tight)  style: mark
node switch "Switch"  right of router

// An edge's text between two children placed against the frame opens the gap
// it crosses, exactly as it does between two ordinary children.
node pipe "Pipeline"  below fresh  gap: wide
node pipe.src "source"    below pipe text  inside pipe left
node pipe.out "artifact"  below pipe text  inside pipe right
edge pipe.src -> pipe.out  "build and test"  from: right  to: left
example-on-the-parent-s-edge__reladraw.svg

Edge labels

An edge's text widens the gap it crosses.

edge labels
// An edge with text widens the gap it crosses.

style plain  fill: theme-fill  border: theme-border

// Nothing here says how far apart these two sit. The default gap is sized for
// two nodes to breathe, not to hold a word, so the text opens it.
node parser   "Parser"                     style: plain
node resolver "Resolver"  right of parser  style: plain
edge parser -> resolver  "statements"  from: right  to: left

// The same two placements with nothing on the edge, for comparison.
node sink  "Sink"   below parser (gap: wide)  style: plain
node quiet "Quiet"  right of sink  level with sink       style: plain
edge sink -> quiet  from: right  to: left

// Going downward the text is only a line deep, so it opens the gap by one line
// and hangs out either side.
//
// Written with `<-`, which draws the same picture as `writer -> disk`. It just
// puts the node the line is about first. `from:` is still the first name
// written, so it names Disk's side.
node writer "Writer"  right of resolver (gap: wide)  style: plain
node disk   "Disk"    below writer                  style: plain
edge disk <- writer  "flushed to disk"  from: top  to: bottom

// Already wider than the text needs, so nothing moves.
node roomy "Roomy"        below sink                                 style: plain
node spare "Plenty spare"  right of roomy (gap: wide)  level with roomy  style: plain
edge roomy -> spare  "fits"  from: right  to: left

// Corner to corner, so there is no single gap the text sits in and nothing is
// widened for it.
node high "High"  right of writer (gap: wide)  style: plain
node low  "Low"   below high  right of high  style: plain
edge high -> low  "diagonal"


// --- What the brackets after a text say ------------------------------------
// `at` is where the block of text sits — one of the nine named positions —
// and `align` is how its lines range against each other once it is there.
// Neither implies the other.

style dim  text: (color: theme-muted)

node shelf "Shelf" (at: bottom-right, align: right)  below roomy (gap: wide)  style: plain
node shelf.one "first"   style: plain
node shelf.two "second"  below shelf.one (gap: tight)  style: plain

// A box is usually the size of its own text, so `at` is inert on one. An icon
// against the box's right side is two lines tall, which gives a shorter text
// beside it room to move. A badge would not: it is placed against the text, so
// it goes where the text goes.
node tall "Top" (at: top-left)     right of shelf (gap: wide)  style: plain
node tall.disk  icon: disk  inside tall right
node deep "Bottom" (at: bottom-left)  below tall (gap: tight)  style: plain
node deep.disk  icon: disk  inside deep right

// `size` and `wrap` are properties of the text too. The wrap is a count of
// characters, not a distance: it says how much fits on a line and nothing
// about where anything sits.
node aside "A quiet remark that folds itself, with [dim]one word[/dim] quieter than the rest." (size: small, wrap: 28)  right of tall (gap: wide)  shape: none

// A literal bracket is written \[, since [ opens markup.
node literal "sizes \[small, normal, large]"  below aside  gap: tight  shape: none

// An edge's text takes the same brackets, less `at`. A size may be a number of
// pixels as well as a name.
edge tall -> aside  "carried" (color: theme-muted, size: 12)  from: right  to: left
example-edge-labels__reladraw.svg

Nested edges

Edges between the same two sides nest instead of crossing.

nested edges
// Several edges between the same two sides nest instead of crossing. The lanes
// are as wide as the texts riding in them.

style plain  fill: theme-fill  border: theme-border

// A reciprocal pair around a corner. Further left along Gateway's bottom is
// further down Queue's left, so the order reverses between the two ends.
node gateway "Gateway"  style: plain
node queue   "Queue"    below gateway  right of gateway (gap: wide)  style: plain
edge gateway -> queue  "publishes messages"  from: bottom  to: left
edge queue -> gateway  "receives messages"   from: left    to: bottom

// Two edges facing each other, where the order runs the same way at both ends.
node client "Client"  below queue (gap: wide)  left of queue  style: plain
node server "Server"  right of client (gap: wide)  level with client  style: plain
edge client -> server  "request"   from: right  to: left
edge server -> client  "response"  from: left   to: right

// Five lanes down a vertical run. The texts all sit at the same height, so the
// only way they clear each other is sideways and a lane has to be a whole text
// wide. These two nodes are wide enough for five of them.
node fan  "Fan of five, spread wide"  below client (gap: wide)  left of client  style: plain
node hub  "Hub of the same five"     below fan (gap: wide)  style: plain
edge fan -> hub  "one"    from: bottom  to: top
edge fan -> hub  "two"    from: bottom  to: top
edge fan -> hub  "three"  from: bottom  to: top
edge fan -> hub  "four"   from: bottom  to: top
edge fan -> hub  "five"   from: bottom  to: top

// Two edges pointing the same way. Nothing tells them apart, so the order they
// are written in decides. Swap these two lines and the texts swap lanes.
node left_wall  "Producer"  below hub (gap: wide)  left of hub  style: plain
node right_wall "Consumer"  right of left_wall (gap: wide)  level with left_wall  style: plain
edge left_wall -> right_wall  "bulk"     from: right  to: left
edge left_wall -> right_wall  "control"  from: right  to: left

// Sharing one side only is not a bundle. The far ends differ, and that is
// enough to order them.
node spoke "Spoke"  below right_wall (gap: wide)  style: plain
node up    "Up"     above spoke  right of spoke (gap: wide)  style: plain
node down  "Down"   below spoke  right of spoke (gap: wide)  style: plain
edge spoke -> up    from: right  to: left
edge spoke -> down  from: right  to: left

// More edges than the two sides can hold. The arrowheads squeeze together
// because there is nowhere further along an edge to go, and each line makes up
// the shortfall by bowing out in the middle, where the texts ride.
node narrow "Narrow"  below spoke (gap: wide)  left of spoke  style: plain
node also   "Also"    right of narrow (gap: wide)  level with narrow  style: plain
edge narrow -> also  "first"   from: right  to: left
edge narrow -> also  "second"  from: right  to: left
edge narrow -> also  "third"   from: right  to: left
edge narrow -> also  "fourth"  from: right  to: left
edge narrow -> also  "fifth"   from: right  to: left
example-nested-edges__reladraw.svg

Parallel edges

Several edges between the same two nodes stay parallel.

parallel edges
// Several edges between the same two nodes with no side named anywhere. Each
// line is shifted sideways off the one a single edge would draw, so they stay
// parallel and each end lands wherever its own line crosses the border.

style plain  fill: theme-fill  border: theme-border

// Level nodes, so the run is horizontal and the lanes fan out vertically.
node a "A"  style: plain
node b "B"  right of a (gap: wide)  level with a  style: plain
edge a -> b  "first"
edge a -> b  "second"
edge a -> b  "third"

// A perfect diagonal, where the single line would leave through the corner
// exactly. These two straddle it, one point on each of the edges meeting there.
node low  "Low"   below a (gap: wide)  left of a  style: plain
node high "High"  above low  right of low (gap: wide)  style: plain
edge low -> high   "up"
edge high -> low   "down"

// A near diagonal, where the crossing point sits on one edge but close to the
// corner.
node near "Near"  below low (gap: wide)  style: plain
node far  "Far"   right of near (gap: wide)  above near  style: plain
edge near -> far  "one"
edge near -> far  "two"

// A lone edge is in no group and does not move.
node solo  "Solo"   below near (gap: wide)  style: plain
node alone "Alone"  right of solo (gap: wide)  level with solo  style: plain
edge solo -> alone  "by itself"

// Naming a side on either end takes an edge out of the group.
node named "Named"  below solo (gap: wide)  style: plain
node other "Other"  right of named (gap: wide)  level with named  style: plain
edge named -> other  "free"
edge named -> other  "free too"
edge named -> other  "top"  from: top  to: top

// More than the edges can hold. The ends squeeze together and each line bows
// out in the middle to make up what its ends could not give it, so the texts
// still come apart. The middle line has no shortfall and stays straight.
node many "Many"  below named (gap: wide)  style: plain
node one  "One"   right of many (gap: wide)  level with many  style: plain
edge many -> one  "first"
edge many -> one  "second"
edge many -> one  "third"
edge many -> one  "fourth"
edge many -> one  "fifth"

// The same on a vertical run, where the texts sit at one height and a lane is
// a whole text wide.
node top    "Top"     below many (gap: wide)  style: plain
node bottom "Bottom"  below top (gap: wide)  style: plain
edge top -> bottom  "alpha"
edge top -> bottom  "beta"
edge top -> bottom  "gamma"
example-parallel-edges__reladraw.svg

Through a gap

Edges sent through the gap between two named nodes.

through a gap
// `between a and b` sends an edge through the gap between two named things, for
// the stretch where it is passing them. Before and after, it goes wherever its
// ends take it.

style channel  fill: theme-page  border: theme-fill

// North and South are apart vertically, so that is the gap. Both edges start
// well to the left, climb into the channel, run along it and leave for East.
// They share it, so they take a lane each.

node west   "West"
node lower  "Lower"   below west     gap: tight
node north  "North"   right of west  gap: wide   style: channel
node south  "South"   below north    gap: wide   style: channel
node east   "East"    right of north and south  level with west  gap: wide

edge west  -> east  "over"   between north and south  from: right  to: left
edge lower -> east  "under"  between north and south  from: right  to: left

// Port and Starboard are apart horizontally, so here the line runs down the gap
// instead. This edge names no sides: the open end aims at the channel rather
// than at the far node, so it leaves Bow already heading for the gap.

node bow        "Bow"        below lower  gap: wide
node port       "Port"       below bow    gap: wide       style: channel
node starboard  "Starboard"  right of port  level with port  gap: wide  style: channel
node stern      "Stern"      below port   gap: wide

edge bow -> stern  "down"  between port and starboard

// The same channel travelled the other way. No text on this one, so its lane
// needs only the room two arrows need.

edge stern -> bow  between port and starboard  from: top  to: bottom

// High and Low sit diagonally, so there are two gaps between them and the file
// has to say which. `vertically` is the gap you measure with a vertical ruler —
// under High, over Low — so a line running along it travels sideways.

node far   "Far"    below stern  gap: wide
node high  "High"   right of far  gap: wide  style: channel
node low   "Low"    below-right of high  gap: wide  style: channel
node near  "Near"   right of low  level with high  gap: wide

edge far -> near  "threaded"  between high and low vertically
example-through-a-gap__reladraw.svg

Outside the nodes

Edges that run past the nodes, and the page grows to hold them.

outside the nodes
// Edges that leave the area the nodes cover. The page grows to hold them: the
// nodes here cover 421x230 and the page comes out 465x328.

style plain  fill: theme-fill  border: theme-border

node a "A"                            style: plain
node b "B"  right of a  (gap: wide)   style: plain
node c "C"  right of b                style: plain

node d "D"  below a and c             style: plain

// A long arch over everything, with the text at the top of it.
edge a -> b  "over the top"  from: top  to: top

// The same shape across a narrow gap.
edge b -> c  from: top  to: top

// Out the left and back into the bottom, so the loop hangs off the left edge.
edge d -> a  "round the side"  from: left  to: left

// And one under the bottom row.
edge a -> d  from: bottom  to: bottom
example-outside-the-nodes__reladraw.svg

Going round boxes

Every routing bug reported so far, each line now going the shortest way round.

going round boxes
// Every routing bug reported so far, one row each, the line's text saying
// where it was reported. Each of these was drawn through a box, on top of
// another line, or hard against a box's edge at some point. Every line now
// takes the shortest way round that passes through none, keeps a lane of its
// own, and keeps its distance.

// Hacker News, 2026-09-26: ends facing away across a row. The line ran
// straight through the middle node, its text sitting on it.
node parser "Parser"
node resolver "Resolver"  right of parser
node renderer "Renderer"  right of resolver
edge parser -> renderer  "Hacker News"  from: left  to: right

// GitHub issue #2: stacked, sides at right angles. The line came straight up
// through the lower node.
node p "P"  below parser  (gap: wide)
node q "Q"  below p
edge p -> q  "issue #2"  from: bottom  to: left

// GitHub issue #2: both ends on the same side, one node behind the other. The
// line went straight through the node behind.
node r "R"  right of p  (gap: wide)
node s "S"  right of r
edge r -> s  "issue #2, same side"  from: left  to: left

node v "V"  right of s  (gap: wide)
node w "W"  below v
edge v -> w  from: bottom  to: bottom

// A fan of three, 2026-09-27. The lines to the top and bottom nodes ran level
// with the middle one, straight through it, out past the column and back.
node a "A"  below q  (gap: wide)
node b "B"  right of a  (gap: wide)
node c "C"  right of a  (gap: wide)
node d "D"  right of a  (gap: wide)
edge a -> b  "fan"  from: right  to: top
edge a -> c  from: right  to: left
edge a -> d  from: right  to: bottom

// The Cache case, 2026-09-27. The curved line dipped through Cache, while the
// same line drawn square happened to clear it.
node web "Web"  right of c  (gap: wide)
node api "API"  right of web
node db "Postgres"  right of api
node cache "Cache"  below api
edge web -> db  "cache case"  from: bottom  to: bottom

// A curve back round the side of a row, 2026-09-26: it crossed the node it
// was going round.
node e "E"  below a  (gap: 200)
node f "F"  right of e  (gap: wide)
node g "G"  below e and f
edge g -> e  "round the side"  from: left  to: left

// Two lines turning down the same gap into one box, 2026-09-27. The longer
// one's curve swung across the other's twice. They now take a lane each, in
// the order that keeps them from crossing at all.
node x1 "X"  right of z  (gap: wide)
node a1 "A"  below x1
node b1 "B"  right of a1  (gap: normal)  below a1  (gap: tight)
edge a1 -> b1  from: right  to: left
edge x1 -> b1  from: right  to: left

// No side named, with a box in the way. The line used to be straight from
// center to center, through whatever lay between.
node x "X"  right of f  (gap: wide)
node y "Y"  right of x
node z "Z"  right of y
edge x -> z  "no sides named"

// Three lines told the same thing, 2026-09-27. "below ra" lines were drawn on
// one path, so the three read as one line with three arrowheads. They now take
// a lane each, nested so that none crosses another.
node ra "A"  below g  (gap: wide)
node rb "B"  right of ra
edge ra -> rb  from: left  to: right  below ra
edge ra -> rb  from: left  to: right  below ra
edge ra -> rb  from: left  to: right  below ra

// Through a named gap, 2026-09-28. The line ran about 2px from Lower, then
// came into Lowest's right side down Lowest's own edge. It now keeps clear of
// both and comes into the side from outside.
node west "West"  right of rb  (gap: wide)
node lower "Lower"  below west  (gap: tight)
node lowest "Lowest"  below lower  (gap: normal)
edge west -> lowest  "between"  from: left  to: right  between lower and lowest
example-going-round-boxes__reladraw.svg

Line styles

Curved, square and straight lines, dashes, and how crossings are drawn.

line styles
// Everything about how a line is drawn, in its bracket: line: (…). A shape is
// the rule for joining what the file fixed — where the line leaves and which
// way it heads, where it arrives, and any sides it passes.

// Out of A's top and into B's left, three ways.
node a1 "A"
node b1 "B"  right of a1  above a1  (gap: wide)
edge a1 -> b1  "curved"  from: top  to: left

node a2 "A"  right of a1  (gap: 220)
node b2 "B"  right of a2  above a2  (gap: wide)
edge a2 -> b2  "square"  from: top  to: left  line: (path: square)

node a3 "A"  right of a2  (gap: 220)
node b3 "B"  right of a3  above a3  (gap: wide)
edge a3 -> b3  "straight"  from: top  to: left  line: (path: straight)

// Rounded corners, and a square line naming no sides: across first, then down.
node c1 "C"  below a1  (gap: 90)
node d1 "D"  right of c1  below c1  (gap: wide)
edge c1 -> d1  "rounded"  from: bottom  to: left  line: (path: square, corners: rounded)

node c2 "C"  right of c1  (gap: 220)
node d2 "D"  right of c2  below c2  (gap: wide)
edge c2 -> d2  "no sides"  line: (path: square)

// A pass: straight bends only where the clause makes it.
node p1 "P"  below c1  (gap: 220)
node q1 "Q"  right of p1  (gap: 220)
node m1 "M"  right of p1  left of q1  level with p1
edge p1 -> q1  "below M, square"  from: right  to: left  below m1  line: (path: square)

node p2 "P"  below p1  (gap: 90)
node q2 "Q"  right of p2  (gap: 220)
node m2 "M"  right of p2  left of q2  level with p2
edge p2 -> q2  "below M, straight"  from: right  to: left  below m2  line: (path: straight, corners: rounded)

// Patterns and thicknesses.
node f1 "F"  below p2  (gap: 90)
node g1 "G"  right of f1  (gap: 220)
edge f1 -> g1  "dashed"  line: (pattern: dashed)
node f2 "F"  below f1  (gap: normal)
node g2 "G"  right of f2  (gap: 220)
edge f2 -> g2  "dotted"  line: (pattern: dotted)
node f3 "F"  below f2  (gap: normal)
node g3 "G"  right of f3  (gap: 220)
edge f3 -> g3  "dash-dot"  line: (pattern: dash-dot)
node h1 "H"  right of g1  (gap: 140)
node k1 "K"  right of h1  (gap: 220)
edge h1 -> k1  "thin"  line: (thickness: thin)
node h2 "H"  below h1  (gap: normal)
node k2 "K"  right of h2  (gap: 220)
edge h2 -> k2  "thick"  line: (thickness: thick, color: #d2904e)
node h3 "H"  below h2  (gap: normal)
node k3 "K"  right of h3  (gap: 220)
edge h3 -> k3  "4, dashed"  line: (thickness: 4, pattern: dashed)

// Crossings: the later line jumps the earlier one.
node w "W"  below f3  (gap: 150)
node e "E"  right of w  (gap: 420)
node n1 "N1"  below g3  (gap: 100)
node n2 "N2"  right of n1  (gap: 30)
node n3 "N3"  right of n2  (gap: 30)
node s1 "S1"  below n1  (gap: 160)
node s2 "S2"  right of s1  (gap: 30)
node s3 "S3"  right of s2  (gap: 30)
edge w -> e
edge n1 -> s1  "arc"  line: (crossing: arc)
edge n2 -> s2  "square"  line: (crossing: square)
edge n3 -> s3  "gap"  line: (crossing: gap)
example-line-styles__reladraw.svg

Line ends

Arrowheads, dots, diamonds and bars at either end of a line, or none.

line ends
// What can be drawn at each end of a line. The arrow is a mark, a line, a mark:
// a word or a glyph at either end of one or two dashes, either end left off.

style label  text: (color: theme-muted)

node plain   "plain"
node plain2  "a -- b"      right of plain (gap: wide)  style: label
edge plain -- plain2

node arrows  "arrows"      below plain
node arrows2 "a <-> b"     right of arrows (gap: wide)  style: label
edge arrows <-> arrows2

node uml     "inherits"    below arrows
node uml2    "a --|> b"    right of uml (gap: wide)  style: label
edge uml --|> uml2

node whole   "composed"    below uml
node whole2  "a diamond--> b"  right of whole (gap: wide)  style: label
edge whole diamond--> whole2

node part    "aggregates"  below whole
node part2   "a odiamond-- b"  right of part (gap: wide)  style: label
edge part odiamond-- part2

node dots    "dots"        below part
node dots2   "a *--o b"    right of dots (gap: wide)  style: label
edge dots *--o dots2

node bars    "bars"        below dots
node bars2   "a |--| b"    right of bars (gap: wide)  style: label
edge bars |--| bars2

// A style can carry marks too. It fills only the ends the arrow leaves blank,
// so here the arrow's own head stays and the style adds a dot at the start.
style tracked  from-mark: dot  to-mark: oarrow
node styled  "styled"      below bars
node styled2 "a --> b, with a style"  right of styled (gap: wide)  style: label
edge styled --> styled2  style: tracked
example-line-ends__reladraw.svg