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 — areas paints a template of named regions and each child takes one with area="…". 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 filling main — falls out. See dashboard.
  • Line numbers — grid-column="1 / 3" / grid-row="2 / span 2" place a child by 1-based grid lines (or a span). 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 (or ordinal, 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)