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 t…
Using reladraw with Org Mode
- 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
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
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
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
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
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
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
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
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
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
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
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
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"
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
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
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
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)
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
