Grammar reference
Every element in the language — the built-in ones from the project README, and the ones the shipped libraries define, in the same list because a library element is an element. Both are generated at build time, the library entries from the components themselves, so neither can drift from what ships.
<diagram> — root
padding (margin, default 24); background (a colour, or transparent);
theme (light | dark pins the palette; auto, or omitting it, follows the reader's prefers-color-scheme);
width / height fix the canvas size (else it's sized to content), either one
alone fixing that axis and leaving the other to size itself. A container root
is made definite by that, so it fills the canvas and justify has slack to
distribute — a slide. A mark root is not: a bigger canvas is more room, not a
bigger box, and stretching the one shape you drew is a change to the picture
rather than to the space around it. A canvas the drawing cannot fit into is
refused rather than cut off at the viewport — a container shrinks into a
definite canvas where it can, and this is the case where it already has, as far
as it goes.
<import name="..."> — bring a library's components into scope
A library is a Sauvignon document of <define>s, looked up by name from a
registry (never a path). Its <style> comes with it. Importing twice is
harmless; a local <define> of the same name wins; two libraries claiming one
name is an error. Shipped: uml, er, c4 — see sauvignon/prelude/. Their
components appear in the grammar reference
in the same list as the built-in elements, because a library element is an
element; each entry names the import it needs. The prose comes from the
component's <doc> and the parameter table is derived from its <define>, so
neither can drift from what ships.
Add your own with register_library(name, source), or
--library NAME=PATH on the command line.
<define name="..."> — a reusable component
Body is one layout root plus optional internal <connect> / <align> /
<distribute>. Use the name as a tag to instantiate; attributes on <define>
are default parameter values.
| in the template | means |
|---|---|
${param} |
substituted from the instance's attributes (or the <define> default) |
<slot/> |
replaced by the instance's children |
id="x" |
scoped to instance-id.x; reachable from outside as such |
<doc>…</doc> |
what the component is for — read by the reference, never drawn |
A component is an element, so the attributes every element takes — width,
height, aspect, class, padding, grow, align-self, justify-self,
grid-column, grid-row, area — fall through onto the template's layout root.
<entity width="200"/> sizes an entity exactly as it sizes a <rect>. An
explicit ${width} in the template wins: threading it somewhere particular is
the author saying where it belongs, and the instance's value then goes there
instead. Anything else the template never interpolates is still refused — a
component's parameters are its own namespace, so a misspelled one fails rather
than landing on the root as dead styling.
A component reaches everywhere a base element does, including inside the
elements that read their own children — so a <participant> that expands to an
<actor>, a <bar> that expands to a <bars> layer, or a <plane> that
expands to an isometric <layer>, all work and render identically to the thing
they stand for. Ids are not rescoped in there: a sequence's ids are its actor
names and a <message> refers to them by exactly what you wrote, so those
vocabularies police their own ids instead (<sequence> refuses two actors
alike). Wiring can't come along — a chart has no layout tree for a <connect>
to live in, and says so.
Marks — the tag is the shape
<rect> (alias <box>, <node>), <ellipse>, <circle>, <diamond>,
<hexagon>, <parallelogram> (alias <io>), <cylinder> (alias <database>,
<db>), <brace>, and <text> (no surrounding shape). Label may be an attribute or
element text (<rect>Start</rect>) — and that holds inside a <sequence> and an
<isometric> too, where it used to be the attribute alone: an <actor id="a">
written with its name as text drew the id, and an isometric tile drew nothing.
<polygon> is the one whose shape you describe rather than name — sides is
what a polygon is, so it's an attribute rather than a tag per count. sides
builds a regular polygon, which is most of them and not a chevron; for an
outline that isn't regular, give the vertices with points
(points="0,0 3,0 4,2 3,4 0,4 1,2"). The numbers need no particular range — the
outline is stretched to fill its box like every other polygon's, so whole numbers
usually read better than sixths — and it is the same PolygonMark the built-ins
are, so a connector clips to the outline. Writing points and one of the four
below is refused: both describe the outline. Until this, an arbitrary outline was
reachable only from Python, which put the whole of PowerPoint's process-and-pyramid
vocabulary outside the language. See smartart.
| attr | meaning | default |
|---|---|---|
sides |
how many, at least 3 | 4 (a square) |
skew |
lean it: slides the top across relative to the bottom | 0 |
distortion |
taper it: widens the top and narrows the bottom | 0 |
orientation |
turn it, in degrees | 0 |
Odd counts put a vertex at the top and even counts take a half-step turn, so a
triangle points up, a square sits flat and six sides come out as the <hexagon>
above — the rule was checked against the named shapes rather than invented
beside them, and it reproduces the registered triangle vertex for vertex. The
vertices are stretched to fill the unit box like every other polygon's, after
those three transforms, so a sheared one still fills the box the layout measured
rather than leaning out of it.
Room for the label follows from the geometry rather than a number per shape: a
regular polygon of circumradius 1 has an inradius of cos(π/n), so padding scales
by 1/cos(π/n) — which lands on 1.63 × 1.77 against the diamond's hand-chosen
1.6 × 1.8, and 1.33 × 1.44 against the hexagon's 1.3 × 1.45. All four attributes
are refused on a shape that already has an outline: there's nothing about a
<hexagon>'s sides left to say. For a regular polygon rather than one stretched
to its label, give it aspect="1". See shapes.
<cylinder> is the one built-in a polygon can't express, so it has a mark class
of its own: it draws as two paths — the silhouette, then the front of its lid,
which is what makes it read as a solid rather than a rounded box. A connector
meets it as a rectangle, which the silhouette is everywhere but the lid and the
base. A stadium needs no shape at all: SVG clamps rx to half the height,
so <rect rx="99"> already is one. See stack.
<image> is the one mark whose content comes from outside the document, and
the reason the whole of SmartArt's picture category was out of reach. Its href
is relative, resolved beside the document that names it — an absolute path is
refused, because a path from someone's disk is exactly what <import name=…>
exists to avoid, while a folder of slide material moves as a unit. The bytes are
then embedded as a data URI, so what comes out is one file you can paste with
nothing to go missing. It takes its own size from the file (PNG, JPEG, GIF and
SVG headers; WebP reports its type and asks for a size), and fit says what
gives when the box isn't the picture's shape — cover crops and is the default,
contain fits the whole picture in, fill stretches. It draws no label: a
caption belongs under it in a <column> or over it in a <stack>, and text in
the middle of a photograph is the one answer nobody wants.
<brace> is the one mark not sized by its own label. A brace says these,
together, so what it has to fit is the thing it is bracing — which it has no way
to measure. It takes the span the way anything else takes a size it didn't
choose, from the line it is in: align="stretch" on the row hands it the row's
depth, and it spans exactly what it stands next to. opens names the side the
arms reach toward, which is the side the braced thing is on — right (a {,
the default), left, top, bottom — and it is refused on a shape with no
arms, for the same reason sides is refused on a <hexagon>. The label goes on
the other side, where the tip points, because centred in the box (as every
other mark's is) it would sit on the curve; <cylinder> moves its label for the
same reason. See transformer.
Whitespace in a label collapses, in both of its spellings. XML already
normalises a literal newline inside an attribute to a space; element text now
agrees, so label="a b" and <rect>a b</rect> are the same label however the
source is laid out — which matters because element text is exactly where a
file's own line-wrapping lives (prose writes one label
across three indented lines). It also fixes a silent loss: a newline used to go
into the SVG untouched, where the renderer's whitespace rules delete it, so a
label broken across source lines drew "QueryAttention" in a box measured for a
character that never rendered. A label needing more than one line gets it from a
width to wrap inside, or a <column> of <text> — not from how it is typed.
A mark holds a label and nothing else. A child element inside one is refused
rather than dropped — <text>size of <tspan>50,257</tspan></text> used to
compile and draw "size of". The refusal for gap on a mark already said "no
children to arrange"; this is the other half of that sentence, and it was
missing.
A <text> mark wraps: it has a real min-content (its widest unbreakable
piece) and max-content (one line), and when given a definite width narrower than
one line — an explicit width, a stretch column, or a grid cell — it breaks
across lines and grows taller. A line may break between words and after a /
or a -, as browsers do; without those a path or a hyphenated name is one
unbreakable piece and falls to the character-level last resort, which cuts
mid-token and reads as a typo (one/two/thr + ee/four). One tokeniser answers
both questions, so min-content and wrapping cannot disagree about what a piece
is. A piece still too wide for the box is split at character boundaries
(overflow-wrap: break-word) so nothing overflows. See prose,
breakword.
A label inside a shape wraps the same way, once the shape has a width to sit
in — an explicit width, a stretch column, a grid cell — and with no width the
shape sizes to the label instead. The room comes from the shape, so a diamond
wraps sooner than a rectangle of the same width. The box grows by a line for
each line, unless its height was decided: a pinned height, a height="%",
an aspect read off the width, a fixed grid row. Then there is nowhere for the
lines to go, and lines that would be drawn over the top and bottom of the box are
refused, naming the height that holds it and the width that would keep the
label on one line. A decided height with room for the lines is left alone, even
if it is tighter than the layout would have made it.
Any element takes an aspect ratio (aspect="16/9", 16:9, or a number):
given one axis (an explicit size, a stretch column, a 1fr grid cell), it derives
the other to keep the ratio — a 16:9 hero, a grid of squares. See
aspect.
A width/height may be a percentage (width="42%"), resolved against the
parent's definite inner size on that axis — proportional segments that tile a
row, a full-height pane. It applies in row/column/stack (grids use fr);
against an indefinite parent it falls back to content. See
proportion.
| attr | meaning | default |
|---|---|---|
id |
name, for connectors | — |
shape |
override the shape independently of the tag (e.g. <node shape="ellipse">) |
the tag |
label |
text inside the shape | — |
label-side |
put the label beside the shape instead: left | right | above | below. The shape keeps its own size (a small dot can carry a long name) and connectors end at the shape, not at the words. The label is set close against the shape, its nearest end 6px off |
inside |
at |
inside a <stack> only: at="x y" places the mark by hand, from the stack's top-left (see below) |
— |
step |
when a host builds the diagram up, the step the mark appears at (a whole number from 1); on a container, everything in it. Sauvignon draws everything and writes data-step for the host (see Building a diagram up) |
from the start |
width / height |
explicit size — px, or a % of the parent's definite inner size (overrides intrinsic) |
fits label |
aspect |
width÷height ratio (16/9); derives the axis left auto |
— |
grow / align-self / justify-self |
flex/grid item controls (see combinators & grid) | — |
padding |
space around the label | 12 |
rx |
corner radius (rect) | 6 |
text-align |
label alignment: left | center | right |
center |
class |
CSS class(es) for styling | — |
| other | fill, stroke, stroke-width, … → per-element inline style |
— |
<row> / <column> / <stack> — combinators
Arrange children along an axis (row = →, column = ↓, stack = superimpose).
gap (default 30; column-gap/row-gap or gap="row col" set the axes
separately), align (start|center|end|stretch, default
center), padding, id (makes the group connectable). Give a width/height
and the main axis gains flexbox-style justify (start|center|end|
space-between|space-evenly|space-around); any child may set align-self
to override the container's cross-axis align (e.g. a right-aligned footer under
a centred title). A <stack> has no main axis, so both of its axes are cross
axes and they are two: justify places and sizes a child across, align does
so down, and either takes a per-child justify-self / align-self. So
justify="stretch" is how a progress-bar track fills the card it sits in, with
the fill laid over the start of it. Both default to the
shared centre it superimposes on. (One word used to drive both, so a child could
be centred or in a corner and nothing between, and justify was accepted and did
nothing at all — which is why two circles offset sideways, the thing a Venn is,
could not be said.)
Placing by hand. A child of a <stack> with at="x y" is put there, measured
from the stack's top-left corner, and the stack grows to hold everything placed
in it; children without at are aligned as before. It is the escape hatch for a
figure someone has already arranged — a hand-drawn graph whose positions are the
point — rather than a way to lay out diagrams in general. On a mark with its
label beside it, at places the mark and the label hangs off it, reaching left
or up past the corner if it has to, so a dot lands where it was put however wide
its name turns out. at anywhere but in a <stack> is refused, and so are
negative coordinates. See linked data.
Filling. When the container has a definite size (an explicit width/
height, or the fixed canvas), a child with grow="true" absorbs the leftover
main-axis space. align="stretch" makes children fill the cross axis —
their line's cross size, so items in a line share a height (the classic
equal-height card grid; see cards). That is the definite
size when there is one, and otherwise the tallest item in the line: a plain row
is a wrapped row with one line, and stretch means the same thing in both.
Written sizes. A child's written height is its height in a column, whether
or not the column's own height is definite, and even below what its type would
take on its own — which is how two lines are set close under each other
(<text height="14"> at 11px; a <text> left to the layout is at least 24px
tall). Nothing in a row or column shrinks, so where the container's main size was
decided — its own width/height or a % of one, the canvas, a fixed grid
track it stretches to — and the sizes written on its children, with the gaps
between them, come to more than it has, the document is refused: the message
names the room, where it came from, and what the children ask for. A child left
to the layout is not counted; content overflows a decided size as it always has.
Wrapping. wrap="true" on a <row>/<column> with a definite main axis
lets children that would overflow break onto new lines (a row wraps downward, a
column rightward); the container grows on the cross axis to fit the lines. Each
line is justify-distributed independently. Without a definite main size it stays
a single line. See tags. When the container also has a
definite cross size with spare room, align-content (start|center|end|
space-between|space-evenly|space-around|stretch) distributes the lines
along the cross axis — the multi-line analogue of justify. See
aligncontent. It distributes a grid's row tracks
the same way and by the same call, since rows are to a grid what lines are to a
wrap. (align is per item and needs no definite size; align-content moves
whole lines and does — see Filling above.)
Charts are lazy — a <plot grow> reflows to render at the size it's given
(a chart that fills a slide, not a fixed box centred in one). See
fillslide and the design note in
docs/layout-models.md.
<grid> — 2D track layout
Children flow row-major into a grid of tracks. columns is a track list
(default: a single auto column); rows is optional (else one auto row per wrap). A track is a fixed length
(120), a fraction (1fr), or content-based (min-content | max-content);
repeat(n, …) expands. Cells share tracks, so columns line up across rows —
the alignment a column-of-rows can't guarantee. fr tracks split the leftover
space (so they need a definite width, as grow does); min-content sizes to
the largest min-content width in the column — for wrapping text, its longest word. gap spaces both axes; cells stretch-fill their track, and a
child with an explicit size — px, or a % of its cell — keeps that size at the
cell's top-left (so %-width bars in a 1fr column read as a horizontal bar
chart). See table, cellpercent.
Placement. By default children flow row-major, but a child can place itself explicitly, and a name/range repeated across cells spans those tracks:
- Named areas —
areaspaints a template of named regions and each child takes one witharea="…". Rows are separated by/(XML normalises attribute newlines to spaces, so line breaks won't do);.is an empty cell; areas must be rectangular. The app shell — header/footer spanning the width, a chart fillingmain— falls out. See dashboard. - Line numbers —
grid-column="1 / 3"/grid-row="2 / span 2"place a child by 1-based grid lines (or aspan). Explicitly-placed children reserve their cells and the rest auto-flow into what's left — a 2×2 feature tile beside auto-flowed siblings (a bento grid). See bento.
<grid columns="150px 1fr" rows="56 1fr 42"
areas="header header / sidebar main / footer footer"> … </grid>
| attr | meaning | default |
|---|---|---|
columns |
column track list (e.g. "max-content repeat(3, 1fr)") |
one auto column |
rows |
row track list | auto (max-content per row) |
areas |
named-area template (/-separated rows); children use area="…" |
— (row-major flow) |
gap |
spacing between tracks — both axes, or two values "row col"; column-gap/row-gap set one |
30 |
id |
makes the grid connectable | — |
A child in a grid may set area="name", or grid-column / grid-row ("2 / 4",
"2 / span 2", "3", negatives from the end like "1 / -1"); otherwise it
auto-flows into the next free cell. Within its cell, a child is aligned by
justify-items/align-items on the grid (x and y) or justify-self/align-self
on the child — start | center | end | stretch (default). See
toolbar. Lines are
1-based and negatives count from the end, so grid-column="1 / -1" spans every
column — a full-bleed row without naming areas (masthead).
<graph> — layout worked out from the connectors
The one container that decides for you. Give it marks with ids, <connect>
them, and it places them so the edges flow downward and cross as little as it can
manage — nothing in the source says where anything goes. gap spaces nodes
within a row, row-gap between rows (both as for combinators); padding and
id work the same way too. Also available as <contains layout="graph"> when
you want the boundary drawn around it.
A <contains> may be the graph — layout="graph" on it makes the boundary
and the layout one element rather than two, which is what a Mermaid subgraph
is. See 8008, a flowchart LR translated whole.
direction turns the flow: down (default) · right · up · left, and
Mermaid's and Graphviz's codes (td, tb, lr, bt, rl) too. Only the
downward layout is computed — flowing rightward is that same layout with every
node's size transposed and the offsets transposed back, and up and left are
mirrors — so layer assignment, crossing reduction and straightening stay one
implementation rather than four that can drift apart. See
flowchart-lr.
This is Sugiyama, Tagawa and Toda's 1981 framework: reverse the back edges a
depth-first search finds so the rest is acyclic, layer by longest path from a
source, give an edge that skips rows a lane on each row it passes (a placeholder
ordered and placed with the nodes, then dropped, so the edge keeps a clear way
down between them), order each row by the barycentre of its neighbours, then
relax the coordinates without letting a node pass the one beside it. A curved or
orthogonal connector between two of the graph's children, going with the flow,
leaves by the side the graph flows out of and comes in by the side it flows into
-- the bottom and the top of a downward graph -- and keeps to those sides when
it has to go round something. Only connectors
between this graph's own children shape it — one leaving the graph is still
drawn, it just does not move anything. A child stands for everything inside it,
so an edge to a node nested in a child counts as an edge to that child: a dot with
its label beside it (label-side) is a row of the two, and it is placed by the
edges that reach the dot. The same holds for <radial> and <network>. A cycle
is drawn with an edge pointing back against the flow rather than refused.
The heuristics are the standard cheap ones and the sweep counts are small, which is right for diagram-sized graphs and would not be for large ones. Crossing reduction is a heuristic because the exact problem is NP-hard; coordinates are a local relaxation rather than Brandes–Köpf. See algorithms/drawing-the-graph.
<radial> — rings out from a centre
The other container that decides for you, and the one <graph> can't be: a
mindmap has a middle and grows outward in every direction at once, so flowing
it downward buries the thing it's about in the top-left. Give it marks with
ids and <connect> them, exactly as for a graph.
The node no edge points at is the centre; depth is the ring. Each subtree takes
a wedge in proportion to how many leaves it holds — not how many nodes,
because leaves are what need room on the rim, and counting nodes crowds a deep
thin branch against a shallow bushy one. Each ring is then pushed out until it
clears both the ring inside it and the arc its own nodes need (at radius r a
wedge of θ gives r·θ), which is what keeps the labels apart. Several roots —
or a pure cycle, which has none — share a centre that isn't drawn, so a forest
is still one picture. gap spaces nodes along a ring, row-gap between rings;
also available as <contains layout="radial">. See
mindmap.
<network> — no flow, no centre, just the relationships
The third container that decides for you, and the one neither of the others can
be. <graph> flows one way and <radial> grows out of a middle; services that
call one another, or a set of related ideas, have neither — and drawn as a graph,
whichever node you happened to write first becomes the top of a hierarchy nobody
claimed.
This is stress majorization (SMACOF), what Graphviz's neato uses by
default: the nodes are placed so the distance between every pair on the page is
as close as it can get to the number of edges between them — not just for pairs
that share an edge, which is why it settles into something even rather than
something merely untangled. Breadth-first search from each node gives those hop
counts; one hop is worth the room a node actually takes, since points need no
such scale and rectangles do. Each pass moves every node to the weighted average
of where its neighbours would put it, and stress falls monotonically, so a fixed
number of passes gives one answer rather than a different answer per run —
there is no randomness in it at all, the starting circle being written order.
Majorization places points, so overlapping boxes are then pushed apart along whichever axis they overlap least, giving up as little of the shape as it can. Nodes with no path between them have no hop count to honour, so each connected piece is laid out alone and the pieces are set side by side — the only arrangement that says plainly that they are not connected.
gap is the whole of what you set: the clearance between any two nodes, and the
room one edge is worth. row-gap and direction are refused — there are no
rows and no flow — rather than taken and dropped. Also available as
<contains layout="network">. Cost is O(passes × n²) with a fixed pass count,
which is right for a diagram and would not be for a graph of any size — the same
trade <graph> makes. See network.
<isometric> — stacked planes in an isometric projection
Arranges children in a scene rather than on the page. Each <layer> is a
plane floating above the next, its contents lying in that plane; <link from=".."
to=".. .. .."> joins an object on one plane to one or more below.
On <isometric>: angle (degrees, default ~26.57 — the 2:1 isometric), tile
(an object's default side), gap (air between planes), thickness (a slab
skirt; 0, flat, by default), tile-upright="true" to keep the tiles' label
text unsheared — a switch, since labels already says what <bars> and
<points> write on each mark, and tile- because a <layer> also
carries a label of its own down the gutter, which this does not touch. A layer
overrides the scene either way round, so tile-upright="false" puts one plane
back.
On <layer>: label (drawn down the gutter); plane="none" to draw the contents without a plane —
a switch, not an index, and any other value is refused, since layers stack in
the order they are written and are not numbered; flow="run" for a contiguous
strip (each child's width a proportional weight scaled to the layer's
length, which is read by that flow alone and refused without it); otherwise
rows and flow="right"\|"down" fill a grid across-then-down or
down-then-across. tile-width, tile-height and tile-padding are defaults
for the children, and say so: on a <row>, width is the row's own, and these
are not — a layer whose children are 30 wide comes out 313, a number nobody
wrote. padding is the layer's own: the air between its contents and the
plane's edge.
On a child: id, label, class, its own width/height/padding,
region="a b" (the fraction of its text block that is filled — a tint of the
mark, since the tile's own label is drawn on top of it and the mark itself is a
mid-tone no ink clears in both schemes), and ragged="true" for a torn edge.
On <link>: shape="prism" (the default — four lines joining matching corners)
or "line", which suits a one-to-many fan and leaves from along the source so
it shows where in it each target picks up.
<isometric>
<layer label="presentation" flow="run" plane="none" length="600">
<rect width="510" class="x"/>
<rect width="2073" id="P1" label="P1" class="p"/>
</layer>
<layer label="document" rows="2" flow="down" tile-width="30" tile-height="34" tile-padding="4">
<rect/><rect id="p13" region="0.42 1"/><rect id="p14" region="0 1"/>
</layer>
<link from="P1" to="p13 p14" shape="line"/>
</isometric>
The projection is one matrix per plane — x = u − v, y = (u + v) · tan(angle)
— carried on a group, so rectangles and their labels are projected together.
Four things it works out for you, each of which looks right in the source and
wrong on screen: a w×h object projects w + h wide, so clearance is
reckoned after projecting; planes are separated by the edges that actually
meet rather than by the box around a plane, so a long ribbon and a wide slab
interleave instead of one clearing the other's whole diagonal; a run is only
seamless along a plane axis (squares set side by side
across the screen are rhombi meeting at a point); and in a wrapped grid the
row separation is kept under the column pitch, so a sequence never
backtracks past a whole column — which is what makes links cross. Draw order is
interleaved bottom-up, so a link passes behind the object it hangs from and
in front of the one it lands on. Style with .sv-iso-plane, .sv-iso-tile,
.sv-iso-region, .sv-iso-link. See isometric and
homebase.
<sequence> — lifelines down the page, messages across it
The one picture the ordinary machinery can't hold. Everywhere else a connector
attaches to a shape; here the second axis is time, and a message attaches
to a point partway down a lifeline — a place no mark exists at. So the geometry
is computed, as <isometric>'s is.
Each <actor id label> gets a header box and a dashed lifeline. Each
<message from to> takes the next row down and is drawn through anything
between its ends, which is right here and is exactly what the obstacle-avoiding
router would refuse to do. A message to its own actor loops out and back, since
a horizontal line to itself has no length. label, dash, arrow and head
work as on <connect>; activate / deactivate open and close an activation
bar on the actor doing the work, nesting when a call re-enters. A deactivate
with no bar open is refused rather than dropped; being left open is fine and
runs the bar to the foot. Inside a <frame> this bites: the compartments are
alternatives to each other, but the bar is one shape down one page, so a bar
opened before the frame closes once on the frame's last row, not once per
compartment. A <note over="a b"> takes a row of its own, because the second axis is time
and a note written between two messages belongs between them; over one actor
it spreads either side of that lifeline, over several it reaches from the first
to the last. autonumber numbers the messages — notes are not counted, since the count is of
things that happen and a note is written about what happens, and the count runs
straight through a <frame> so a number is unique in the diagram and can be
cited out loud. gap spaces the lifelines, row-height the message rows. A column
widens to fit the longest label — or note — crossing it.
<frame kind="…" label="guard"> draws a labelled box round the rows inside it —
UML's alt, opt and loop — and <else label="guard"/> divides one into
compartments with a dashed rule where it is written. Frames nest by nesting, and
a nested one is drawn inset within its parent. A frame covers only the lifelines
its own rows touch, and the diagram grows to hold a box that reaches past them.
kind is any word: the box is identical for alt, opt, loop, par and the
rest, and only the tab word differs, so there is one element and no closed list.
kind="retry" is as good as kind="loop".
<sequence gap="76">
<actor id="browser" label="Browser"/>
<actor id="api" label="API"/>
<message from="browser" to="api" label="GET /orders" activate="true"/>
<frame kind="alt" label="in cache">
<message from="api" to="browser" label="200 (cached)"/>
<else label="cache miss"/>
<message from="api" to="browser" label="200 OK" dash="4 3" deactivate="true"/>
</frame>
</sequence>
See sequence.
<contains> — a boundary relation
Draws a titled boundary around its children and is itself connectable by id.
| attr | meaning | default |
|---|---|---|
id |
connect arrows to the whole group | — |
label |
title drawn at the top | — |
layout |
how children are arranged inside (row|column|stack|grid|graph) |
column |
gap, align, padding |
as for combinators | padding 16 |
fill, stroke, … |
boundary style | light panel |
color, font-size, font-weight, font-family, font-style |
style the title text | 13px grey |
The title takes the same text styling as a mark, so a section gets a prominent, colour-matched heading. See sections.
<connect> — an edge (aliases: <edge>, <arrow>, <link>)
| attr | meaning | default |
|---|---|---|
from / to |
source & target id (a mark or a group); the same id at both ends draws a self-loop |
required |
from-anchor / to-anchor |
pin to a named anchor: n s e w ne nw se sw center, or the long forms top bottom left right |
auto |
routing |
straight | orthogonal (alias elbow) | curved (alias bezier) |
straight |
arrow |
which ends bear a head: end | start | both | none (a plain line / wire) |
end |
head / tail |
which shape the head is, at the to / from end (see below) |
arrow |
label |
text on the edge, mid-route | — |
from-label / to-label |
text near that end — an ER or UML cardinality | — |
moves |
whether it shapes a computed layout, or is only drawn — false for a relation that is real but is not a step |
true |
step |
the build step it appears at, for a host that builds the diagram up | the later of its two ends' |
stroke, stroke-width, dash |
line style | — |
Arrowheads. In a class or ER diagram the head is the meaning — a hollow triangle says "inherits from" the way no label has to — so the shapes are named for what they draw and left to the diagram to say what they mean:
head / tail |
drawn | the usual reading |
|---|---|---|
arrow |
filled triangle (the default) | flow, direction |
open |
two lines, unfilled | association, dependency |
triangle |
hollow triangle | generalisation / inheritance |
diamond |
filled diamond | composition |
odiamond |
hollow diamond | aggregation |
crow |
three prongs fanning out at the shape | "many" (ER cardinality) |
circle |
hollow circle | "zero" / optional |
cross |
an X across the line's end | blocked, cancelled, severed |
bar |
a single stroke across the line | "one" |
A filled head stops one pixel past the line's end, so its tip laps over the
shape it points at and no hairline of background shows between them; its fill is
the line's own colour, so the lap does not read as anything. A hollow one
stops short of the end. It cannot lap — its fill is the page, and that pixel
would be a page-coloured notch in the shape's border — and it cannot stop level
either, because its outline is a second visible stroke, and two strokes drawn on
the same line both straddle it and cross. It anchors so that the far point of its
miter lands on the shape's path — (w/2) / sin(half the apex angle) past the
vertex the join turns, which is 1.92px on the generalisation triangle where half
a stroke would be 0.75. A miter tapers, so that last fraction is too thin to see:
the visible tip stops just outside the path and the border covers the rest.
arrow says which ends carry a head; head and tail say which shape, at
the to and from ends respectively. So many-to-one is arrow="both"
head="crow" tail="bar", and arrow="none" draws a bare line whatever the
shapes say. Only the heads a diagram actually uses are defined in its SVG. See
classdiagram, erd.
Ports. When several connectors would attach to the same side of the same shape, they're spread along that side instead of stacking at one point, ordered by where their far ends lie so they don't cross — for a curve bent round something, by where it actually comes from. A lone connector still lands on the side's midpoint, so this only changes diagrams that had the problem. On a circle or an ellipse the ports are on the outline itself, and a curve arrives at each one aimed at the centre. Where a side is too short to keep an arrowhead apart from a line leaving beside it — two connectors on a small dot are only a few pixels apart — a curve moves to another side facing its far end, if one is clear and roomy; arrows side by side, or lines fanning out side by side, stay put.
Obstacle avoidance. A connector routes around any shape between its ends
rather than through it, via a visibility graph over the obstacles' corners (see
the algorithms notes). This holds
for a curved edge and an orthogonal one too: an arc bulging off its chord and
an elbow running between its ports are both drawn blind to what sits between. A
curve that would cross something first tries other curves: every side of each
end that faces the other, reaching further out before it turns, and the shortest
that clears wins — so an arc can leave a dot by the bottom, dip under a label and
come in from the left. Where no curve clears, or the edge is an elbow, it falls
back to the same detour a straight edge takes and is drawn rounded to match. (The elbow used to be skipped — the check read
"is this a curve?" while the comment beside it said "is this a straight edge?" —
so it drove through in silence.) A shape enclosing an endpoint isn't an obstacle —
an edge legitimately crosses the panel it starts inside — and a clear route stays
perfectly straight (a clear curve stays a curve).
Attachment. A connector meets a shape where the centre-to-centre ray exits it, which for two shapes offset from one another can be within a whisker of a corner. Attachments are biased back into the middle half of the side they land on; ones already there don't move, so a facing pair still meets edge-centre to edge-centre. A circle or an ellipse has no corners, so it is left alone: a line into one points at its centre, and so does a detour's last step.
Labels. An edge label sits mid-edge — of the line you can see, not of the
route: an arrowhead covers the last several pixels of its end, so centring on the
route puts the label low by half a head. It sits on the line at whatever
angle, letting the halo it paints punch the gap, and steps off it only when it is
wider than the gap it sits in —
widening the gap instead would make a diagram as wide as its longest label, and
layout does not consult the connectors anyway: combinators place children and
connectors are drawn between the results, which is the seam <graph> sits on the
other side of. The default gap of 30 is checked against what a labelled
connector needs (12px of type, 9px of arrowhead, 4.5px clear each side) rather
than left as a number that happens to work. from-label / to-label instead sit a fixed distance in from each
shape, so a cardinality reads at the end it describes — the thing one
mid-edge label can't say. On an edge shorter than twice that distance they're
capped to a fraction of it, so the pair can't meet in the middle. They also step
clear of the line, where a mid-label sits on it: a mid-label names the line, so
the line running behind it reads as what it is, while an end label names the
junction and would otherwise crowd the line exactly where the head already is. Where that would lap over a shape or another label, it's moved to the
best of a set of candidate positions along and either side of the route; a label
with nothing in its way doesn't move.
Nudging. Two connectors between the same pair of shapes would otherwise trace the same line end to end, leaving only the top one visible. Routes that run along each other (as opposed to merely crossing, which is fine) are fanned apart, and their ends put back on their shapes.
When there is no way around. In a crowded corner the visibility graph can
find no route at all — every waypoint swallowed by another obstacle. The edge is
still drawn, straight, because one tight spot shouldn't fail a whole diagram and
an edge crossing what it starts among is a real case; but it's marked
sv-edge-crossing so it can be styled or found, rather than passing for a clean
line.
Pinning from-anchor/to-anchor opts out of ports, avoidance, the corner bias and nudging: an explicit anchor
is an exact point, and the line between two of them is left alone, for the cases
where it's meant to pass through what it crosses (a circuit's wires, say). Per
end: pin one and that end leaves from the anchor named while the other still
picks its side and takes a port there, so a fan-in with one pinned end still
spreads. (An elbow used to read the pair — pinning one end of one produced a
route byte-identical to pinning neither, while a straight edge honoured it.) What
an elbow between two pinned anchors still owes them is the direction: a
connector pinned to a north side leaves heading north, so n-to-n arcs over
the top of both shapes rather than running along their borders. Each end takes a
16px stub straight out along its own side before it turns, and the crossbar goes
past both shapes when the ends leave the same way, through the gap when they
leave opposite ways and face apart, and around the corner when they're square.
<align> — a hard alignment constraint
Constrains the referenced ids to share an edge or center line, overriding their structural placement on that axis.
The first id is the anchor and does not move; the rest move to it. That was
always how it compiled — every other id is related to the first — but which one
actually gave way used to fall out of the order the structural constraints
happened to be built in, which was leaves-first. So a hard constraint reached up
through the element it named and dragged that element's whole ancestry into
place, and the constraint left to give was an ancestor's place in its own
parent: <align of="l2 m5"> moved a third column named nowhere in it, and grew
the canvas by the distance it had travelled. They are now ordered anchor-chain
first and outermost-first within that, so the placement that gives way always
belongs to something named as a mover.
Naming a container moves the group intact — everything inside travels with it, which is how an aside column is put level with a box in the trunk. Naming a child moves only that child, out of its own stacking order: right for two boxes in a column sharing a left edge, wrong for a module that has neighbours. Align what you want to move.
| attr | meaning |
|---|---|
of |
space/comma-separated ids (≥2); the first is the anchor |
edge |
left | right | top | bottom | center-x | center-y |
<rank> — ids on the same graph row
Holds the referenced ids on one row of a <graph>. Rows come from how far a node
is from a source, which is the right default and the wrong one for two things
meant as peers that no path happens to connect: they land on different rows and
read as stages of each other.
Made before coordinates exist, unlike <align>, which moves finished geometry
— so it is done by contracting each group to a point and layering the contracted
graph, rather than by nudging depths afterwards, which needs a pass and then the
pass after that. Contraction also says when the request is impossible: a path
from one member to another is a cycle once contracted, and a direct edge between
two of them is refused by name, since contraction would otherwise turn it into a
self-loop and drop it. Ids that don't all share one <graph> are refused too,
because doing nothing quietly is indistinguishable from not working. Pair it with
moves="false" when the edge between two peers is a relation rather than a step.
| attr | meaning |
|---|---|
of |
space/comma-separated ids (≥2), all in one <graph> |
<distribute> — even spacing along an axis
Places the referenced ids in order with an equal gap.
| attr | meaning | default |
|---|---|---|
of |
space/comma-separated ids (≥2), in order | — |
axis |
x or y |
x |
gap |
edge-to-edge spacing | 30 |
Attributes are checked. A chart element refuses an attribute it doesn't
read, naming the ones it does — so a near-miss (x-titel), or a plausible
option borrowed from another tool (orientation, y-scale, autonumber),
is an error rather than a silently ignored word. Where the idea exists
elsewhere in the language the message says where.
<data name="..."> — an inline dataset
Holds <record> elements; each record's attributes are its fields. The tag is
<record> rather than <row> because <row> is the layout combinator — one
name, one meaning — and anything else in a <data> block is an error rather
than being silently ignored.
<plot data="..." width height> — a chart frame
Owns the scales, draws the axes, hosts mark layers. id makes the chart
connectable like any box. coord="polar" projects a <bars> layer into a pie.
| attr | meaning |
|---|---|
axes |
true (all) · false (none) · x / y (that side's labels only) · frame (the L-shaped lines, no ticks) |
x-title / y-title |
axis titles (y-title is rotated) |
y-max |
cap the y-axis |
x-min / x-max |
pin the linear x domain (numeric x, no <bars> layer), so points/lines don't rescale to the data range |
Mark layers (children of <plot>), each binding channels to fields:
| layer | channels | renders |
|---|---|---|
<bars> |
x (band), y, fill, labels, hover |
bars from the baseline (or pie slices, in polar) |
<points> |
x, y, fill, r, labels, label-size, hover |
a scatter of circles |
<line> |
x, y, stroke |
a polyline (a closed loop in polar — a radar) |
<area> |
x, y |
a filled band down to the baseline (a filled radar in polar) |
<boxes> |
x (band), y, weight, fill |
a box plot per category: quartiles, median, whiskers, outliers |
<rule> |
x1 y1 x2 y2 |
a straight reference line in data coordinates |
Boxes summarise. Every other layer draws a row: a bar is one, and two rows in
one slot would hide each other, which is why <bars> refuses them. <boxes> is
the one layer that draws all the rows in a slot as one mark — a box from the
first quartile to the third, the median across it, a whisker out to the furthest
value within one and a half box-lengths of each end, and a dot for each value
past one (Tukey's box plot). The quartiles interpolate between neighbouring
observations, which is what numpy, R's default and a spreadsheet's QUARTILE.INC
all do, so a box drawn here agrees with one computed anywhere else. weight says
how many observations a row stands for, and reads as repetition — a row of weight
three gives exactly the box that the value written out three times would — which
is how to box a column too long to write: each distinct value once, with its
count. It must be a whole number; a category whose weights are all zero has
nothing to summarise and is refused. The box is the span a y2 bar draws, so
coord="flipped" lays it on its side for free; a polar frame is refused, since
it would bend q1-to-q3 into a wedge read by its angle. A box takes fill and
colors like a bar, and its class lands on every part of it. There is no
labels or position: one mark per category leaves nothing to label beside or
arrange against. See boxes in the tutorial.
Position. position says how a layer's several series sit relative to one
another: group (the default) sets them side by side, stack sits each on the
running total of the ones below, and fill does the same on shares of each
column's total, so every column is full height and the value axis reads in
percentages. It's a position adjustment in the Grammar of
Graphics' sense — a component beside the mark and the frame, not an option
belonging to bars — so it's spelled the same on every layer and meets every
coord. Stacking is only ever "draw from the running total instead of from
zero", which is why one rule serves bars and areas alike. And it's what makes the
line at the top of this README describe the language: a pie is
polar × bars × stack, its slices already sitting end to end around the
circle. Asking for group in polar gives you the coxcomb that phrase implies:
side by side around the circle means each category takes an equal share of the
angle and the value runs outward, where end to end gives each an equal radius
and runs the value around. Its radius is linear in the value, as every other
value mapping in the language is — so a wedge's area grows as the square, and a
rose flatters its largest values. Nightingale drew hers on area for that reason;
taking the square root here without being asked would be applying a scale nobody
named, which is what x-scale / y-scale exist to avoid. (Asking for fill in
polar is accepted and draws the same pie, because there the circle is the
total.)
fill is implemented as a transform of the data rather than of the drawing —
divide each value by its column's total and stacking does the rest — which is
also the honest description of it: filling does not draw differently, it
re-expresses what is drawn. Values it cannot express are refused: a negative has
no share of a total, and a column of nothing but zeroes has no total to take a
share of (it would draw as an empty column among full ones, which reads as a
value rather than an absence). A filled layer cannot share an axis with an
unfilled one either — one is in shares, the other in the data's own units. See
stacked, proportional, coxcomb.
A y with several fields (y="Strider Aragorn") draws one shape per
series, ordinal-coloured with a legend: side-by-side sub-bars for <bars>, and
a line, band or set of dots each for <line>, <area> and <points>. (labels
is refused alongside several fields — it writes one thing per row, and there is
more than one.)
Every layer takes a class, so one series can be styled on its own — the same
bargain <connect class> makes for a drawn relation that isn't a node. So do
<actor>, <message>, <note>, <frame>, <else> and <rule>, and
<diagram class> lands on the root <svg> for an embedding page to aim at.
labels says what to write beside each mark, and means the same thing on
<bars>, <points> and <treemap>: true for the mark's own value, a field
name for that column, false (or absent) for nothing. A field missing from any
row is refused by name, and a long one turns in at the edge rather than hanging
off the figure.
It used to be a field on <points> and a bare flag on the other two — one word,
two capabilities. A polar frame refuses it outright: a
per-mark label sits past a mark's outer end, and neither a pie's slices nor a
radar's spokes have one.
On <points>, label-size sets the labels' font size in pixels (default 12):
sixty named points at the default size are a pile of text, and a font-size on
the built-in sv-pointlabel class is refused in a <style>, since layout
measures a label before it draws it. The label sits half its own height above
the dot, and the plot reserves room for it at that size. Written without
labels there is nothing to size, so it is refused.
hover names a field whose text a browser shows when the pointer rests on a
mark, written into it as an SVG <title>: on <points>, <bars> (each of a
row's bars, when it has several series) and <chronology>'s moments and
stretches. Where labels writes on the picture, hover keeps the picture clear
-- a scatter of a hundred chapters can say which chapter each dot is without a
word on it. A row may leave the field out and gets no title; a field no row has
is refused.
fill says what colours a mark, and says it the same way on every graphic
that draws one — <bars>, <points>, a pie, a rose, a radar, <treemap>,
<spine>, <histogram>, <timeline>, <chronology>. It reads, in this order:
- a column in the data → colour by it. Values that are themselves colours are used verbatim (the data brought its own, and a palette would overrule the author one row at a time); a numeric column takes a sequential ramp; any other column takes an ordinal palette and draws a legend.
index(orordinal,rainbow,auto,lane) → a generated ramp, one colour per mark by position.- anything else → a literal colour, used as written.
A column beats the keyword, so data with a column called index can still be
coloured by it — the rule labels keeps for the same reason. Either way colors
is the scale's range: two ends for a ramp (colors="#eef #08519c"), a palette
for categories (colors="#a, #b, …").
A name that is in some rows and not others is a typo, not a colour, and is
refused by the row that lacks it. And fill is refused where it would have
nothing left to colour: alongside several y series, which the palette
already tells apart, and on a radar, where a column has no per-row mark to
vary over because the ring is one shape across every row.
x is a linear axis when the field is numeric and no <bars> layer is present (bars always get a band), otherwise band.
A field name that is not in the data is a typo, and every attribute that takes
one says so. x, y, fill, labels, weight, by, value, source, target,
row, start, end, section, group, group-value, left, right — the
refusal names the attribute, the field, and the row that lacks it, which is
the part that matters: a column present in the first record and missing from a
later one is the mistake that actually happens, and "not in the data" alone
sends you to the wrong record. A fill that is in no row is a literal colour
on a cartesian layer or a timeline, which is the one deliberate ambiguity here;
it is still a typo when the name is in some rows and not others. A value that is
present but is not a number is a different mistake and gets a different sentence,
naming its row and showing the value; nan and inf are refused with it, since
a chart draws lengths and neither is one.
A colour has to be one. fill, stroke, color and every entry of
colors are checked: hex, any CSS colour function — rgb(), oklch(),
color-mix(), var(), and whatever CSS adds next, which pass on their shape —
or a word CSS names (tomato, rebeccapurple, transparent). A bare word that
is none of those is refused, because an unpainted mark on a white ground is an
absent mark and used to go without a word. The same rule is what lets a column
of red and blue be read as the author's own colours rather than as two
categories to hand to a palette.
Scales. x-scale / y-scale take linear or log. Band-or-linear is read
off the data — a categorical field gets a band, a numeric one a linear axis — but
a log axis can't be: nothing in the numbers says you want one, it's a choice
about what you're showing, so you name it. Equal ratios then take equal space,
which turns steady multiplication into a straight line. Gridlines fall on
decades, subdivided 1‑2‑5 while there's room to read them. A log axis has no zero
and no negatives — there's no point on it for them — so those are refused rather
than clamped into a lie, and a bar grows from the decade under the data instead
of from an origin that doesn't exist. See logscale.
Axis labels thin, gridlines don't. A tick ladder is chosen from the data — five round numbers, the 1-2-5 rungs of each decade, every month boundary — and knows nothing about how long the axis turned out or how wide the numbers did. Where they won't all fit, every nth label is kept, so what's left is still a ladder: dropping whichever two collide gives a sequence like 0, 100, 220, 340, where every gap is legal and none of them is a step. The gridlines all stay — an unlabelled one reads as a minor division, where a missing one would say the axis stopped.
Coordinate frames. coord is cartesian (the default), polar or
flipped, and it changes the frame, never the layers — which is why a
horizontal bar chart isn't an option on <bars>. flipped measures the value
across the page instead of up it: the independent axis runs down the left, the
value axis along the bottom, and titles follow their own field onto whichever
axis now draws it. It applies to a line, an area, a scatter and grouped series
alike, because a frame applies to whatever is drawn in it. Reach for it when
category names are long — upright they crowd the axis, flipped they read
straight. See flipped.
Polar. coord="polar" reads the same layers in a different frame, and
which graphic comes out is the layer's doing rather than a chart type.
<bars> makes the value an angular sweep — a pie. <line>, <area> or
<points> make it a radius along one of the spokes — a radar, where the
rows are the spokes rather than positions on a shared axis, a line closes back
to its first spoke, and several y fields overlay as separate shapes with a
legend. y-max caps the outermost ring, and a value past it crops to the rim
rather than spilling over the labels; it must be above zero, since the
spokes run outward from there and a lower cap would clamp every point to the
centre. Asking for both readings of one value is refused.
The frame has no x and y axes, so the attributes that name or bound them —
axes, x-title, y-title, x-min, x-max — are refused here rather
than ignored, as x-scale/y-scale already were. They were being taken and
dropped, which is how axes="false" sat on the tutorial's pie for four steps
doing nothing, and how axes="bth" passed in polar while the cartesian frame
raised on it. y-max is refused on a pie for the same reason and kept on a
radar, which does have a radius to bound.
The rings are the radial axis, so they stand at the round numbers that axis would tick to and each carries its value, written up the vertical — the one direction these charts leave clear, a rose's first wedge starting at twelve o'clock and a radar's first spoke running along it. Unlabelled rings say there is a scale without ever saying what it is, which is most of the way to a decorative chart. Each number brings its own ground rather than a halo, because a rose's wedges are filled and a halo thins that collision rather than removing it; and they thin out when they will not fit, keeping the outermost, the same way the time axis fits its labels. A ring with no number is still a guide. A pie has none of this: its radius is constant and the value is the angle, so a ring there would measure something the picture is not saying. See pie, radar, coxcomb.
<facet data="..." by="field"> — small multiples
One plot per distinct value of by, laid out as a <grid> of lazy panels
(repeat(N, 1fr)) with a shared y-scale. Same layer children as <plot>.
width/height are the panels' preferred size; columns sets the grid width.
Because the panels are lazy, a content-sized facet draws them at the preferred
size, but a facet in a definite width reflows every panel to fill it (axes
recomputed, not stretched) — the shared domain is data, the sizes are the grid's.
x-title / y-title are drawn once beside the grid, not once per panel: the
panels share one scale, so one title is what the picture means, and a title
pushed down into each panel's own gutter would either appear n times or make
the first column narrower than the rest. They are built out of the ordinary
layout — a rotated <text> in a <row>, a plain one under a <column> — which
is what rotate on a <text> exists for.
<diverging data="..." row="f" left="f" right="f"> — back-to-back bars
A diverging bar chart: per row, a left bar grows left and a right bar grows
right of a shared centre axis (one linear scale). left-label/right-label set
the column headers; colors="#a, #b" the two colours; width/height the size.
(Colour lists split on commas or spaces, keeping parenthesised groups intact, so both oklch(70% 0.1 30) and rgb(10, 20, 30) survive as one colour.)
<histogram data="..." y="count" bin-size="10"> — binned distribution
Gapless bars from per-bin counts, with x-title / y-title (rotated) axis
titles and an optional leader-line callout (annotate-title, annotate-label)
to the last bar. Single-series bars elsewhere accept fill="index" to colour
each bar by position (a generated oklch ramp, no legend).
<timeline data="..." lane="f" start="f" end="f"> — a swimlane / Gantt
Each distinct lane value is a row; every data row is a duration-block from
start to end on a shared time axis.
The axis is a clock or a calendar, read from the data rather than from an
attribute. H:MM:SS, MM:SS or bare seconds is elapsed time: the axis starts at
zero, tick is in seconds — chosen from the span if you don't name one, landing
6-10 gridlines across it on the divisions a clock has (15s, a quarter hour, six
hours) — and labels are MM:SS. YYYY-MM-DD
(optionally \u00a0HH:MM[:SS], always read as UTC so a render doesn't depend on the
machine) is a date: the axis starts at the earliest block, gridlines fall on
day, fortnight or calendar-month boundaries, or on 1 January once the labels are years, to suit the span, and labels coarsen
from 5 Jan to Mar 2026 to 2019 — a Gantt. On a calendar, tick is in
days. Where the gridlines go is a question about time, but whether each one
can be labelled is a question about pixels: a crowded axis keeps every nth
label rather than stacking words on top of each other, and the last one turns to
the inside of the right edge rather than being clipped. Every gridline is still
drawn — an unlabelled one reads as a minor division, where a missing one would
say the axis had stopped. Mixing the two in one timeline is refused. See
gantt, phases.
| attr | meaning | default |
|---|---|---|
lane (alias row, by) |
field whose distinct values are the rows | required |
section |
field grouping the lanes under headings — a second level over lane, for a plan whose workstreams belong to phases. A heading takes a row of its own, and a row is a lane within its heading, so the same lane name under two headings is a row under each: the same workstreams in every phase, or the same sources set out under every event |
— |
start / end |
timecode fields | start / end |
fill |
a literal colour (all blocks) · a colour-valued field (used verbatim, and the row label is tinted to match) · a categorical field (ordinal colour scale) · lane/index (per-lane oklch ramp) |
--sv-mark |
tick |
gridline interval, seconds (a calendar reads it as days) | chosen from the span |
extent |
axis end (timecode); else the last end |
— |
sort |
true (or alpha) = alphabetical · time = by first appearance · omitted = first-seen order; anything else is refused |
— |
bar-height |
row pitch (block height is half) | 40 |
label-size |
row-label font size | scales with bar-height |
x-start |
left label gutter — a minimum, widened to hold the row labels, which are right-aligned against it and would otherwise run off the canvas | 205 |
top |
centre of the first row; at least half a bar-height, since the row reaches that far above it |
50 |
period-label |
the row label for the <period>s, in the gutter beside them |
— |
width / height |
canvas (height derives from the lane count). A height too short for the rows and the axis labels under them is refused rather than cropped | 1125 / auto |
A <period start="…" end="…">label</period> child sets the scene for the rows: a
labelled stretch of time, such as where someone was or which release was in
testing, that is context rather than one of the things being compared. The
periods are drawn as a <chronology> draws its stretches, on a line of their own
above the lanes: each a bar along the line, called out above it (or below, with
side="below") and joined to its label by a leader. Each side's labels make one
row in date order, each centred over its bar unless a neighbour is in the way; a
crowd is spread about its own middle, so no leader crosses another or a label,
and the leaders lengthen as far as twice their usual reach when a label has had to
move far from its bar. Sending alternate labels below the line halves a crowd; a
side too crowded for one row stacks its labels in levels, as a chronology does.
Periods with the same label on the same side, with no other label of that side
between them, share one label with a leader to each bar: a place returned to is
named once. A
dashed guide drops from each end of each period through every lane to the axis,
passing behind the labels, so a block far below can be read against them. Where
one period ends as the next begins, a two-pixel gap at the guide keeps them two
bars. fill colours one period's bar, leader and guides (default --sv-muted),
so periods of one kind can be told from the rest, and a period with no label is a
bare bar. A period counts toward the axis's start and end like a block, and must
be a date or a timecode as the blocks are.
Reproduces all three Digital Tolkien timelines from one element: the Episode
Timeline (lanes = episodes, blocks = a character's speech lines in one race
colour, timeline); the Region Timeline (lanes =
regions, colour-coded per region with tinted labels, sort="time" so the rows
follow the story's journey, regiontimeline); and
the Speech Timeline (lanes = an episode's whole cast, coloured by race,
speechtimeline).
<chronology data="..." at="f" label="f"> — dated events, called out
The other kind of timeline: one axis of years with things pinned to it. A record
with an at is a moment (a dot); one with a start and an end is a
stretch (a bar along the axis). Each is called out by its label — a bold first
line — and an optional detail under it, above or below the line as the record's
side field says, with a leader from the callout to its mark. A stretch's
bar lies on the same side of the axis as its callout, so a stretch above the line
and one below it can cover the same years and both be seen. Stretches on the
same side that overlap stack out from the axis, the earlier against it and a
gap between, so neither hides the other, and the years lift clear of a stack
above the line.
The callouts are placed the way the Digital Tolkien primary-timeline renderer
places them, in date order: each centred over its mark -- or hung from its
leader by its left or right edge, as the record's align field says -- if that
is free,
otherwise in the nearest gap beside a callout already on that level (never
further than one label-width from its mark), otherwise out on the next
level from the axis. The marks never move. Both sides start the same distance
from the axis, so the picture balances, and a chronology too short for the
levels its callouts need is refused, saying how much height they take,
rather than drawn with text off the canvas.
| attr | meaning | default |
|---|---|---|
at / start / end |
the fields giving a moment, or a stretch's two ends, in years (1954.5 for mid-year). A row with both kinds, or neither, is refused |
at / start / end |
label |
field for each callout's bold first line | label |
detail |
field for the lines under it, wrapped | — |
side |
field saying above or below |
all above |
hover |
field whose text a browser shows over a moment's dot or a stretch's bar (an SVG <title>). A row may leave it out |
— |
align |
field saying left, right or center: a callout hung from its leader by its left edge (reaching right of its mark), its right edge (reaching left), or centred over it. The leader meets it just inside that edge, so it stays straight, and two marks close together can send their callouts apart. A row may leave it out |
all centred |
attach |
field giving a year along a stretch where its leader meets the bar, so its callout can clear a neighbour's. A row may leave it out; one on a moment, or outside its stretch, is refused | the bar's middle |
fill / colors |
colour, read as everywhere. An author's colours (a colour column, a literal, colors=) carry onto the callout's text; a generated palette colours the marks only, since it is the chart's invention and text in it may not read |
--sv-mark |
from / to / tick |
the axis ends and its step | a round step clear of the first and last date |
label-size |
the callout's type size (detail at 0.9 of it, the years at 0.8) | 20 |
label-width |
where a callout wraps; its lines are then balanced, so a wrapped callout does not end in a lone word | 220 |
mark-size |
a moment's diameter (a stretch's bar is 0.3125 of it, on its side of the axis) | 16 |
width / height |
canvas; height defaults to what the callouts need | 1125 / auto |
See chronology: the discipline and the corpus, the history of humanities computing above the line and Tolkien's books below.
<sankey data="..." source="f" target="f" value="f"> — flows by width
A band's width is its value, on one scale shared by the diagram, and what
arrives at a node leaves it — conservation is the whole claim a Sankey makes, so
the layout is computed from the values rather than drawn. Columns come from the
links (a node sits one right of everything feeding it), a node is as tall as the
larger of what enters and leaves it, and each column is ordered by the barycentre
of what it connects to, as the layered graph is. gap spaces nodes in a column,
node-width sets the bar, colors overrides the palette. End columns label into
a gutter and middle ones above their node, so no name is printed across a band. A
cycle unrolls into columns with one link pointing back, rather than being refused.
See sankey.
<treemap data="..." by="f" value="f"> — nested rectangles by area
Cells sized so area is proportional to value, laid out squarified (Bruls,
Huizing & van Wijk 2000): rows are grown while doing so improves the worst
aspect ratio in them and closed at the first turn for the worse, so cells come
out near-square rather than as slivers whose areas the eye can't compare.
fill colours by a category field (else each cell takes its own palette
colour), colors overrides the palette, gap is the gutter, and labels writes a value
after each name — true for the cell's own, or a field for that column. A cell carries only what fits inside it — the
value is dropped first, then the name — and its ink is chosen from its own
colour, since five of the eight palette colours are too light for white to clear
AA. See treemap.
<spine data="..." group="f" value="f"> — grouped equal-width bars
A sequence of equal-width bars (e.g. book sections) partitioned into spans
(chapters). Each group draws a grey background bar of its group-value, as
wide as the sections it holds, with a tick and number below; the foreground bars
take value for height and fill (a category field, colors="#a, #b" the
palette) for colour. Reproduces Project Amaze's per-section frequency chart —
see here.
<c4-element> — from c4
From the c4 library — <import name="c4"/> first.
The shape the other four are built from — a name, a bracketed kind and a description on one tinted card. Reach for it directly only for a kind C4 does not name.
| parameter | default | |
|---|---|---|
box |
optional | c4-box |
descr |
optional | (empty) |
kind |
optional | (empty) |
title |
optional | (empty) |
width |
optional | 190 |
<person> — from c4
From the c4 library — <import name="c4"/> first.
A C4 person: whoever uses the system, tinted darker than the software around them.
| parameter | default | |
|---|---|---|
descr |
optional | (empty) |
kind |
optional | Person |
title |
optional | (empty) |
width |
optional | 190 |
<system> — from c4
From the c4 library — <import name="c4"/> first.
A C4 software system — the whole of something, seen from outside.
| parameter | default | |
|---|---|---|
descr |
optional | (empty) |
kind |
optional | Software System |
title |
optional | (empty) |
width |
optional | 190 |
<container> — from c4
From the c4 library — <import name="c4"/> first.
A C4 container: a separately deployable or runnable thing. Put the technology in the kind, as kind="Container: Go".
| parameter | default | |
|---|---|---|
descr |
optional | (empty) |
kind |
optional | Container |
title |
optional | (empty) |
width |
optional | 190 |
<external> — from c4
From the c4 library — <import name="c4"/> first.
A system outside the boundary you are describing, greyed so the edge of your own responsibility reads at a glance.
| parameter | default | |
|---|---|---|
descr |
optional | (empty) |
kind |
optional | External System |
title |
optional | (empty) |
width |
optional | 190 |
<entity> — from er
From the er library — <import name="er"/> first.
An ER entity: a titled header over its attribute rows. Give the connectors between entities crow's-foot heads, and from-label / to-label for the cardinality at each end.
| parameter | default | |
|---|---|---|
title |
optional | (empty) |
<class> — from uml
From the uml library — <import name="uml"/> first.
A UML class box: a titled compartment over a rule, with its members projected below. Pair it with head="triangle" for generalisation, "diamond" for composition and "odiamond" for aggregation.
| parameter | default | |
|---|---|---|
title |
optional | (empty) |
<interface> — from uml
From the uml library — <import name="uml"/> first.
A class box marked as an interface above its name.
| parameter | default | |
|---|---|---|
stereotype |
optional | interface |
title |
optional | (empty) |