Timeline syntax
A reference for the .tl file format used by Timeline Builder.
Every file opens with a keyword that picks the diagram type —
timeline (horizontal, the default), timeline-s
(snake / vertical wrapping), timeline-o (closed
ring), radial-diagram (record-aggregation ring), or
family-tree. After the opener come config lines
written as key: value, then element lines written
as type | id | … with pipe-separated fields.
Diagram types
The first meaningful line of a .tl file is the
diagram-type keyword. The builder picks one of three renderers
based on this opener:
timeline— horizontal timeline. Streams stack vertically, value drives the X axis. Junctions are implicit: any node listed in two or more streams becomes a junction at that column automatically. Adds parallel-band stacking when streams share consecutive nodes, mixed per-stream ribbon widths, and a fade-in rule for streams that start or end at a junction. This is what most of this page describes. See the timeline section for the additional rules.timeline-vertical— same grammar astimelinebut rendered with the grid rotated 90°, so time flows top → bottom while every text label stays upright. See the timeline-vertical section.timeline— legacy horizontal timeline kept for backwards compatibility. Uses an explicitjunction | …keyword instead of the implicit-junction rule. Prefertimelinefor new diagrams.timeline-s— a wrapping snake layout, or a strict vertical spine atwidth: 1 nodes. Document order drives placement; no streams, no junctions, no axis. Supports segments (button / chevron, drawn as continuous shapes that follow the snake through row breaks). See the timeline-s section.timeline-o— closed-ring layout. N nodes sit at equal angles clockwise from 12 o'clock, in document order. Descriptions always render outward; the ring radius is computed automatically from node count + description size. Supports segments (button / chevron, drawn as arc bands on the ring). See the timeline-o section.radial-diagram— record-aggregation ring. The ring is divided into sectors (one persectionline), and each sector contains nodes whose+lines render either as curved text labels or as coloured magnitude bars. Streams own the colour palette and the legend. Sectors are equal-sized by default. See the radial-diagram section.family-tree— descendant chart. UsesNode/Partner/Childdeclarations and a different layout engine that centres children below their parents. See the family-tree section.org-chart— organisational hierarchy. A singleNodedeclaration per person, each naming its manager's id, so it maps directly onto an HR-system export. Colour cascades from manager to report. See the org-chart section.
Most of the config and named-field grammar is shared. Where a diagram type ignores or reinterprets a field, the relevant section below calls that out.
File structure
Every .tl file has the same shape: the opening keyword
timeline, then any number of config lines, then any number of
elements. Blank lines and lines beginning with # are ignored.
timeline value-format: date date-format: YYYY axis-marks: years 25 node | n1 | 1969 | label:Moon title:Apollo 11 node | n2 | 1977 | label:Voyager title:Voyager launched node | n3 | 1981 | label:Shuttle title:STS-1
Grid layout
Every diagram lays content onto a grid. There are currently two grid types: a rectangular flexible grid for linear diagrams that read along an axis, and a polar grid for circular diagrams that wrap around a centre. The grid is what positions nodes, reserves space for description boxes, and decides when the canvas needs to grow.
How content sits on the grid
Each node is plotted at the intersection of two grid squares — never in the middle of a square. Its content goes into a rectangular container that occupies 90% of one square's width, centred on the node's intersection (the midpoint between the two neighbouring squares). Two adjacent nodes each take 90% of their own square with a 10% gap between them, so their containers can never touch even when the descriptions are long.
The container's height is dictated by the content of that node — the title's line count, the body's wrap, an avatar or label. The container's width (i.e. the square size that every other cell on the grid will copy) is dictated by the node with the most information: the renderer measures every node up front, picks the largest, and sizes every square on the grid to fit it. Every square is therefore the same size, so the widest description in the file drives the spacing for every other node.
Rectangular vs polar
- Rectangular flexible grid — used by
timeline,timeline-s,family-treeandorg-chart. Squares are axis-aligned and step along the time / hierarchy axis; containers sit above or below each intersection. - Polar grid — used by
timeline-oandradial-diagram. Squares are annular sections (a slice of an annulus) that radiate from the canvas centre, and the angular pitch is the equivalent of the "square width" on the rectangular grid.timeline-omaps values linearly onto the circle — the smallest value lands at 12 o'clock, the largest closes the seam there too, so a segment34 → 37connects back to a segment1 → 4when the file's value range adds up to a full turn. Segments don't need nodes to exist for theirfromandto; they sweep the arc between those values directly.
To see the grid for any diagram, click Show grid
in the builder's header (or add display-grid: on to
the file). A blue overlay draws each square's envelope, the axis
or ring, and the radial spokes / vertical guides — useful for
diagnosing why a box landed where it did, or for spotting a node
that's sized differently from its neighbours.
title
An optional title: config line draws a heading above the
diagram. Available on every diagram type — timeline,
timeline-s, timeline-o,
radial-diagram, family-tree and
org-chart. Omit it for no heading.
timeline title: Apollo programme axis-marks: years 2 node | n1 | 1967 | label:AS-204 title:Apollo 1 node | n2 | 1969 | label:Apollo 11 title:First lunar landing node | n3 | 1972 | label:Apollo 17 title:Final crewed landing
value-format and date-format
value-format tells the parser how to read the value column.
number (the default) treats values as plain numbers.
date reads them as dates and requires a companion
date-format declaring the field shape.
Date formats
YYYY · YYYYMMM · YYYYMMMDD ·
YYYYMMMDD HH:MM. Month is written as a three-letter
abbreviation, e.g. 1969Jul20.
timeline value-format: date date-format: YYYYMMMDD node | a | 1969Jul20 | label:Moon title:Apollo 11 lands node | b | 1969Nov19 | label:12 title:Apollo 12 lands
spacing
even (default) places each unique value at a uniform pitch
regardless of numeric distance — good for readable layouts where the
data is roughly evenly spaced.
scale places nodes proportionally on the axis, so a node at
year 1000 and a node at year 1900 will be far apart.
node-size
Integer from 1 (default, smallest) up to 20.
Each step adds 4px of radius (1 → 10px, 2 → 14px, 3 → 18px, …,
20 → 86px). Everything that's tied to node size scales with it:
regular circles, junction circles, stream bars, and progress rings.
node-style
circle (the default) renders each node as a circle with
an inner dot. hexagon renders the same node as a flat-top
hexagon at the same effective radius — same layout, same icon / avatar
/ progress behaviour, just a different outline shape.
hidden skips the ring + dot + stem entirely
— useful when segments or chevrons carry the structure and you want
events to read as floating description boxes anchored at their values.
node-style applies to every diagram type. On
family-tree and org-chart the
hexagon value swaps each
person's circle to a hexagon outline. Progress-ring nodes always
render as circles regardless — the arc geometry is circle-only.
axis-position
Where the timeline's horizontal axis sits.
centre (default) places it midway between streams.
above raises it above the first stream — tick labels
flip to sit above the line so they stay outside the streams.
below drops it beneath the last stream.
hide removes it entirely — useful when streams alone
carry the structure.
axis-marks
Controls where tick marks and labels appear on the axis.
nodes places a tick at every node value.
Date-based formats accept years N, months N,
days N, or centuries N where N is the step size.
Numeric values accept just an integer step (e.g. 10).
If omitted, no auto-marks are drawn.
timeline value-format: date date-format: YYYY axis-marks: years 50 node | n1 | 1800 | label:Start node | n2 | 1900 | label:Middle node | n3 | 1950 | label:Mid-century node | n4 | 2000 | label:End
segment-style
How a segment renders.
pill (default) draws a rounded pill straddling the axis
line with the label inside.
era draws a translucent full-height background band — best
for marking large temporal periods.
chevron tiles each segment as a right-pointing arrow; the
first segment is flat on its left edge, every later segment notches
to receive the previous arrow tip. Often paired with
node-style: hidden to show events as floating descriptions
between the chevrons.
stream-style
How a stream renders.
line (default) draws a thin 2px line connecting the stream's
nodes.
stream draws a thick coloured ribbon whose thickness equals
the node diameter; node circles sit on top as cut-outs along the bar.
description-side
A config line that sets the default side for description boxes.
above (default), below, or alternate
(flips for each node in turn). An individual node can override the
default by carrying its own description-side:above or
description-side:below named field.
connector-style
The connector is the line linking a node to its description
box. connector-style sets how it is drawn — on
timeline, timeline-s and
timeline-o:
dotted(default) — a thin dashed line.line— a thin solid line.lollipop— a stick rising from the timeline along the box's left edge to the title, with a filled head at the title.t— a solid line with a short crossbar at its midpoint, forming a T.arrow-to— a solid line with an arrowhead at the description end (points at the box).arrow-from— a solid line with an arrowhead at the node end (points back at the node).title-underline— a solid line that meets the box; the title gets a coloured underline with the body below it.drop-pin— a map-style teardrop pin at the node end, with a line on to the box.
Every connector inherits the colour of the band it springs from: the segment the node sits in wins, then the stream it rides, then the node's own group as a fallback. The connector also starts touching that band, so a thick stream or a tall segment reads as the visible anchor for the line / pin / arrow.
The connector auto-lengthens to clear a thick band: it grows with
stream-size on a stream-style: stream ribbon,
and with segment-size on a pill or
chevron band, so the description box always sits outside it.
With node-style: hidden the plain line and
dotted connectors are dropped (a stem to nothing reads as
orphaned), but a decorative connector — lollipop,
drop-pin, arrow-to, arrow-from
or t — still renders.
description-container and description-shape
Two config lines controlling how a node's description box is drawn —
on timeline, timeline-s and
timeline-o. They are independent: description-container
sets the fill, description-shape sets the outline.
description-container: solid-white(default) — a white box with a thin border.description-container: invisible— no fill and no border; the text floats free.description-container: solid-dark— the box is filled with the node's group colour and the text turns white.description-shape: rounded(default) /square/circle/hexagon— the outline shape. The shape fits the text box, socircleandhexagonsuit short descriptions (text is laid out the same regardless of shape).
legend
How (or whether) to show a legend of the groups used in the diagram.
off (default) shows none.
left places a chip beside each stream where the stream's
label would otherwise sit.
below places a horizontal chip-row beneath the diagram.
timeline-s, timeline-o,
org-chart and family-tree also accept
legend — any non-off value draws the
chip-row beneath the diagram.
stream-order
Controls the vertical lane order of streams.
declared (default) uses the order streams appear in
the file. auto opts in to a barycenter heuristic
that reorders lanes globally and re-sorts each bundle per column
so streams sharing nodes sit adjacent — useful on multi-merge
diagrams where the declared order would otherwise force ribbons
across intermediate lanes.
Both examples below have the same three streams (green, sand,
blue) sharing the merge node merge at value 3 (sand
absorbed into blue). The only thing that differs is the lane
order. On the left, sand sits above green so its merge
ribbon has to cross green to reach blue. On the right, sand is
declared between green and blue — its merge lands on an adjacent
lane, no crossing.
font and background-colour
Two appearance declarations that apply to the whole diagram.
font: sets the typeface used everywhere in the rendered
SVG (axis ticks, titles, bodies, legend labels). The value is passed
through verbatim as a CSS font-family, so any single
family name or comma-separated stack is accepted. Multi-word names
do not need quotes.
background-colour: sets the diagram's background fill.
Any CSS colour works — hex, rgb(), or a CSS named colour.
Both declarations are optional. If omitted, the diagram falls back to
Playfair Display on a #FAF8F5 background.
timeline font: Inter, system-ui, sans-serif background-colour: #eef4f9 node | n1 | 1 | label:Sans title:Inter on a tinted background node | n2 | 2 | label:Stack title:Any CSS font-family works
node
node | id | value | named-fields…
The id is referenced by streams and junctions.
The value is the node's position on the axis
(number or date, per value-format).
Everything after the third pipe is named fields (see
Named fields).
node | n1 | 2020 | label:Launch title:Beta opens body:50 closed-beta users group:blue
segment
segment | id | from | to | named-fields…
A labelled range on the axis between two values. Render style is
controlled by segment-style (button or era).
segment | warmup | 1 | 3 | label:Pre-launch group:sand segment | launch | 3 | 7 | label:Launch group:sage
stream
stream | id | label | comma-separated-node-ids | named-fields…
Groups a set of nodes into a horizontal row of its own. Streams stack
vertically in declaration order. Nodes belonging to a stream inherit
its colour for their ring and dot (the box keeps the node's own group).
stream | s1 | Frontend | f1,f2,f3 | group:blue stream | s2 | Backend | b1,b2 | group:sage
Progress nodes
Any node with a progress: field (0–100) renders as a
circular ring instead of a dot. Colour is state-driven, ignoring the
node's group: grey at 0%, blue at
1–99%, green at 100%. The arc fills clockwise from
12 o'clock and the percentage number sits inside the ring.
node | n1 | 1 | label:Done progress:100 node | n2 | 2 | label:In flight progress:60 node | n3 | 3 | label:Not started progress:0
Authoring patterns
Idioms that produce clearer diagrams. The parser will accept either form below — these are recommendations, not syntax rules — but the recommended form usually renders better and reads more naturally in the source.
One shared node, not duplicated terminal events
When a stream ends in a merge, define one shared node that every participating stream lists — that single node becomes the implicit junction. A common antipattern (especially in AI-generated diagrams) is to declare a separate "terminal" node on each stream at the same value: the boxes stack on top of each other and the merge reads as a vertical glitch rather than a smooth flow.
Listing the same node id in two or more streams' node lists is what
makes it an implicit junction. The shared node carries its own
label / title / body, which
render once at the bundle midpoint — there's no need (and no room)
for a separate "merger" node per stream at the same value.
Rule of thumb: the merge is the node. Pick one id, put the event language on it, and include it in every stream that meets there.
Style recommendations
Not rules — the parser accepts anything — but these habits tend to make a diagram read cleanly and look polished.
- Capitalise titles. Give each
title:a leading capital (or Title Case). It reads as a proper heading and keeps the diagram looking deliberate. - Keep
label:short. A label is a tag — a year, a step number, one or two words — not a sentence. - Keep
body:to a sentence or two. Long paragraphs make description boxes tall and uneven. - Let streams own the palette. Set
group:on the stream and let its nodes inherit, rather than repeating a colour on every node. - Match the connector to the density. On busy
diagrams a plain
lineconnector stays calm;lollipopordrop-pindraw the eye, so use them when there are only a few nodes. - Reserve
circle/hexagondescription-shapes for short content — they don't reflow text, so long descriptions will overflow the outline. - For a bold "ribbon" look, pair
stream-style: streamwith a largestream-sizeandlabel-style: on-timelineoron-timeline-edge.
Named fields
Fields after the fixed positional columns are written as
key:value pairs separated by spaces. Multi-word values
can be wrapped in double quotes (e.g. title:"Long heading").
| Field | Applies to | Effect |
|---|---|---|
label |
node, segment, junction, stream-via-id | Short uppercase tag at the top of the description box (or the segment/junction label). |
title |
node, junction | Main heading inside the description box. |
body |
node, junction | Descriptive paragraph beneath the title. |
group |
node, segment, stream, junction | Colour group. Must be one of the 39 names in the palette below. |
description-side |
node | above or below. Overrides description-side. |
progress |
node | Integer 0–100. Converts the node into a progress ring. |
icon |
node | Lucide-style shortcode that replaces the node's central dot. Inherits the group colour, scales with node-size. See the full bundled list in the Icons section. |
avatar |
node | URL of an image to place inside the node circle. Cropped to a circle, fills the area inside the ring. Takes precedence over icon when both are set. Works best with square source images. |
Group palette
39 named colour groups, organised into four families. Use the name
(e.g. group:coral) in any group: field. A
node's ring + dot inherit from its stream's group; its description
box uses the node's own group. Progress nodes ignore the group
entirely.
Sizing, label styles & photos
A set of timeline declarations and fields for richer
visuals. All are optional — a file that sets none of them renders
exactly as before.
node-style: plain
In addition to circle, hexagon and
hidden, node-style: plain draws the node
ring but omits the centre dot — a cleaner marker when the dot is
visual noise.
stream-size
stream-size: 1–20 sets the thickness of
stream-style: stream ribbons (it has no effect on the
thin line style). A single stream can override the
global value with a size: field —
stream | s1 | Label | n1,n2 | size:8. Unset, ribbons
keep their legacy width (the node diameter).
segment-size
segment-size: 1–20 sets the band thickness
of segments — the pill height or the chevron height.
A single segment can override the global value with a
size: field —
segment | s1 | 1 | 5 | label:Phase one size:7. Unset,
segments keep their legacy thickness. Works on timeline,
timeline-s and timeline-o.
title-style
title-style: takes a space-separated list of tokens that
override the default title rendering. Tokens combine freely.
inherit— the title takes the colour of the band the node sits in (segment colour wins, then stream, then the node's own group).- A palette colour —
red,sky,lilac,charcoal, etc. The title fills with that palette colour (the same hex thegroup:field would use). - Any CSS colour — a hex like
#2D6CDF, a function likergb(45,108,223), or a CSS named colour the browser knows. Anything that's not a palette name or a reserved keyword is passed through as a CSS fill value. all-caps(orcaps/uppercase) — uppercases the title text viatext-transform.bold— bumps the title to font-weight 900 (the default is already 700, so this is "extra-bold").
Examples: title-style: inherit,
title-style: all-caps bold,
title-style: #2D6CDF all-caps,
title-style: red bold.
label-size
label-size: 1–20 scales the node
label: field — large where you want the label to
carry the diagram (e.g. a year). Titles and body text are not
affected.
label-style
Controls how the label: field is drawn:
text— small uppercase text in the content box.pill— a rounded chip, filled with the node's group colour, white text, inside the box.button— the same chip with square corners.on-timeline— the label is drawn in white directly on the stream line at the node, instead of in the box. Reads best on a dark or strongly-coloured thick ribbon. Title and body still render in the box.on-timeline-edge— likeon-timeline, but the label hugs the edge of the stream ribbon, on the side away from the description box. Useful for visual effects on thick streams — e.g. years running along the rim of the blue-ribbon "Legend Boats" timeline.
Photos — photo:, photo-shape, photo-position
A node can carry a photo: field (any image URL or
data URI). photo-shape: is a per-node field —
square, tall (a narrow upright
rectangle), hexagon or circle — and
photo-shape as a config line sets the default for
nodes that don't specify one. The photo-position
config line decides placement: above the text,
left or right of it, or
node-marker — where the photo replaces the node's
dot/ring and sits on the stream itself. photo: is
distinct from avatar: (the circular in-node portrait
that fills the node ring).
Both photo: and avatar: need a
direct image URL or a data URI — one that returns image
bytes. A link copied from a Google image-search results page
(google.com/url?…) points at a redirect page, not an
image, and renders as a broken-image placeholder. Right-click the
image itself and copy its address, or use a data URI.
timeline stream-style: stream stream-size: 7 node-style: hidden label-style: on-timeline label-size: 6 stream | s1 | Milestones | group:teal node | n1 | 1 | label:1990 title:Founded node | n2 | 2 | label:2008 title:Restructure node | n3 | 3 | label:2024 title:Today
Icons
Add an icon to any node by setting icon:<shortcode>
in its named-fields section. The icon replaces the node's central
dot and inherits the group's colour. Icons scale automatically with
node-size.
Shortcodes match Lucide where possible, so anything in the Lucide library you'd like added is a drop-in: send the name and it goes into the next release. The set below is the curated starter bundled with the renderer today.
timeline value-format: date date-format: YYYYMMM node-size: 3 description-side: alternate node | n1 | 2025Jan | label:Idea title:The idea body:Sketched on a napkin group:gold icon:lightbulb node | n2 | 2025Mar | label:Team title:Co-founder in body:Found the right CTO group:blue icon:users node | n3 | 2025May | label:Ship title:MVP shipped body:Closed beta opens group:green icon:check
Bundled icons
Use the name in lowercase, exactly as written. Unknown names fall back silently to the regular dot — a typo won't blank the node.
timeline
A separate diagram type — files opening with timeline
use the same horizontal layout as timeline but drop
the explicit junction | … keyword. Instead,
any node listed in two or more streams is implicitly a
junction at that column. The renderer figures out how the
ribbons should stack, curve, and converge from the stream node
lists alone.
Almost every timeline declaration and named field
(segments, descriptions, photos, icons, progress nodes, label
styles, fonts, colours, axis marks, legends) carries over
unchanged. The differences are concentrated in how streams meet:
- Implicit junctions — list a node in
stream | a | … | n1, n2, …andstream | b | … | n2, …and the renderer recognisesn2as shared. - Parallel stacking on consecutive shared edges — when two streams list the same pair of nodes consecutively, their ribbons run as touching parallel bands on that edge instead of crossing curves.
- Per-stream ribbon widths —
size:on a stream sets that stream's ribbon thickness independently; a thick trunk and thinner branches pack at their actual widths inside a stacked bundle. - Fade-in / fade-out at endpoints — when a stream's first or last node is a junction, its ribbon curves INTO the persisting stream(s) at that column instead of stacking with them.
- Metro-map auto ordering —
stream-order: autoreorders each bundle's stack per-column using a barycenter sweep to reduce crossings.
A junction | … line inside a timeline
file is ignored — the parser surfaces an amber warning so authors
know to delete it. Lane assignment, axis marks, segments and every
other config key behave identically to timeline.
Stream-nested grammar
Nodes are declared on their own lines after the
stream they belong to — the stream line carries no node-id list
of its own. Every node | … line attaches to the
most recently opened stream | … block; the block
stays open until the next stream | …,
segment | …, or top-level config line. Indentation
is optional — it's only there to make the block visually
obvious; flush-left node lines are equally valid.
A node id only needs full content the first time it appears. Subsequent appearances under other streams are id-only references and the editor shows them as locked — you can't accidentally type a conflicting value or label on a repeat occurrence. The parser uses the first declaration; if a repeat line carries extra fields it's quietly dropped and the editor flags the line in the gutter.
node | … lines that appear before any
stream | … declare orphan nodes — they render on
the axis without belonging to any stream.
segment | …, legend | … and every
key: value config line always live at top level
and close any open stream block.
The legacy flat shape
(stream | id | label | n1, n2, … | named) is no
longer accepted — the parser throws with a pointer to the
migration script:
node migrate-streams.js path/to/file.tl
(in the timeline-builder/ directory).
Implicit junctions
Two streams that share one or more node ids stack at the shared
column. The shared node's label /
title / body render once, anchored on
the bundle's geometric centre; each participating stream still
draws its own marker on its own ribbon.
timeline spacing: even node-style: circle axis-position: hide stream-style: stream stream-size: 4 description-side: alternate legend: left stream | a | Stream A | group:blue node | n1 | 1 | label:Start node | n2 | 2 | label:Sync title:Shared milestone node | a3 | 3 | label:A only stream | b | Stream B | group:amber node | n1 node | n2 node | b3 | 3 | label:B only
Consecutive shared = stacked parallel bands
When two streams list the same pair of nodes
consecutively (here both share n1 → n2), the renderer
draws their ribbons as flat parallel bands across that edge. On
transitions where the bundle changes shape (a stream joins or
leaves), the ribbons curve smoothly into / out of the bundle.
Mixed ribbon widths
A stream's size: sets its own ribbon thickness. In a
stacked bundle, every stream packs at its actual width — a thick
trunk and a thin branch stack side by side with no gap and no
overlap. The description-box stem clears the full bundle height,
whatever the mix of widths.
timeline spacing: even node-style: circle axis-position: hide stream-style: stream description-side: alternate legend: left stream | trunk | Release | size:6 group:blue node | n1 | 1 | label:Start node | n2 | 2 | label:Pilot node | n3 | 3 | label:GA node | n4 | 4 | label:EOL stream | beta | Beta program | size:2 group:green node | spec | 1 | label:Spec node | n2 node | n3 node | post | 4 | label:Postmortem
Fade-in / fade-out at junction endpoints
When a stream's first or last node is a junction, the renderer treats it as a fader rather than a peer in the stack. At each junction column it classifies streams as:
- Passing — has both a previous and a next node in its own sequence (continues straight through). Anchors the bundle.
- Departing — starts here, continues past. Anchors when nothing passes.
- Arriving — ends here. Always a fader.
Anchors stack at the bundle centre as normal. Faders collapse their Y to the bundle centre and their ribbons curve INTO the anchor(s) over the first / last edge — visually merging instead of stacking. Fader markers are omitted at the junction column (they would otherwise sit on top of the anchor's). The description-box stem sizes to the anchor stack height only, so the connector clears the visible markers without overshooting.
Lane choice matters here. The anchor's Y at the junction is the bundle centre; if you declare the anchor stream between the streams it merges from, its post-junction ribbon runs flat across the rest of the diagram. Declare it before or after and it has to curve back to its own lane.
timeline spacing: even node-style: circle axis-position: hide stream-style: stream stream-size: 4 description-side: alternate legend: left stream | a | Stream A | group:blue node | a1 | 1 | label:A start node | a2 | 2 | label:A mid node | merge | 3 | label:Merge title:Streams converge stream | c | Stream C | group:green node | merge node | c1 | 4 | label:C step node | c2 | 5 | label:C end stream | b | Stream B | group:amber node | b1 | 1 | label:B start node | b2 | 2 | label:B mid node | merge
A and B both end at merge (arriving); C starts there
(departing) and is the only anchor. A and B fade into C's Y, and
because C is declared between A and B, C's post-merge
ribbon runs straight across to its own lane.
start-mode / end-mode — opting into stack layout at junctions
By default a stream that departs from a shared node spawns from the column's "origin line" — a single point that the renderer collapses every spawning stream onto. That works for forks where a single root branches outward (the Roman→England/Scotland/Wales example below), but it's wrong when several streams should appear to start side-by-side as adjacent ribbons.
Setting start-mode: stack on a stream tells the
renderer to place that stream in its own stack rank at its first
node, exactly as if it were passing through. When every stream in
a column opts in, the column renders as a parallel-touching
ribbon stack from the very first column.
end-mode: stack does the symmetric thing at a
stream's last node. Both default to origin if not
set.
The two values can mix at a single column — some streams stack, others collapse — so you can lay out one branch as parallel ribbons while another fades into the bundle centre.
timeline spacing: even node-style: hidden stream-style: stream stream-size: 4 axis-position: hide stream | a | A | group:red start-mode:stack node | n1 | 1 node | n2 | 2 node | a3 | 3 stream | b | B | group:blue start-mode:stack node | n1 node | n2 node | b3 | 3 stream | c | C | group:green start-mode:stack node | n1 node | n2 node | c3 | 3
Without the start-mode: stack on each stream the
three ribbons would emerge from a single point at n1 (the fork
look); with it, they appear as three adjacent ribbons that fan
out into their own lanes after n2. Drop the override on any one
stream and that ribbon falls back to spawning from the origin
line while the others stay in their stacked positions.
stream-order: auto — metro-map crossing minimisation
The default is stream-order: declared — lanes are
assigned in file order and every bundle stacks streams in that
same order. stream-order: auto opts in to a two-pass
barycenter heuristic: first, lanes are reordered globally so
streams that share nodes sit adjacent; second, each bundle's
stack is re-sorted per-column so each stream sits close to where
its adjacent-column Ys live. Useful on many-stream diagrams with
complex partnerships; a no-op when declaration order already
matches the data.
Known limitation: the barycenter heuristic can oscillate in
perfectly symmetric many-partner topologies (e.g. four streams
partnered as {A↔D, B↔C}). The algorithm runs but
may not reduce the crossing count for those cases.
Supported configuration and named fields
Every config key and named field that timeline
accepts is also valid on timeline, with the same
semantics: title, value-format /
date-format, spacing,
node-style / node-size,
axis-position / axis-marks,
segment-style / segment-size,
stream-style / stream-size,
connector-style, description-container
/ description-shape / description-side,
label-size / label-style,
title-style, photo-position /
photo-shape, legend,
stream-order, font,
background-colour.
junction | … lines are ignored with a
warning.
timeline-vertical
A 90°-rotated variant of timeline. Time flows
top → bottom instead of left → right, while
every text label stays upright. Grammar, config keys and named
fields are identical to timeline — just
swap the opener keyword.
Under the hood the renderer asks renderTimeline to
produce a horizontal SVG, then turns the result on its side via
a single translate(H 0) rotate(90) on the outer
<g>. Each <text> element
receives a counter-rotation around its own anchor so glyphs
remain readable. No new config keys, no second layout engine.
A few visual quirks fall out of the rotation:
description-side: abovesits to the right of the stream column;belowsits to the left. (The renderer doesn't rename the config — "above" and "below" describe the original horizontal orientation.)- The first declared stream lands on the right side of the canvas, the last on the left. Reorder stream declarations to flip.
- The title sits on the right edge, vertically centred, rather
than at the top. Use
title:sparingly in vertical layouts, or omit it.
timeline-vertical title: Deployment checklist spacing: even node-style: circle node-size: 1 axis-position: hide stream-style: stream stream-size: 3 description-side: alternate legend: left node | n1 | 1 | label:Backups title:Backups confirmed node | n2 | 2 | label:Approval title:CAB sign-off node | n3 | 3 | label:Freeze title:Code freeze node | n4 | 4 | label:Staging title:Deploy to staging node | n5 | 5 | label:Canary title:1% canary node | n6 | 6 | label:Prod title:Deploy to prod node | n7 | 7 | label:Verify title:Post-deploy checks node | n8 | 8 | label:Sign-off title:Release closed stream | release | Release | n1, n2, n3, n4, n5, n6, n7, n8 | group:blue
timeline-s
A separate diagram type — files opening with timeline-s
lay out nodes in a snaking pattern down the page. Each row holds a
fixed number of nodes; rows alternate direction (left-to-right, then
right-to-left, then left-to-right…) and are joined by smooth U-curves
hugging the canvas edge. Set width: 1 nodes to get a
strict vertical spine instead.
Nodes render in document order — the order they
appear in the file. The value field is parsed but
doesn't affect node position; there's no global time axis in a
wrapped layout.
Segments are supported. The renderer matches nodes
by their value falling in the segment's
from/to range and draws one visual per
row span, so a segment that crosses a row break renders as
multiple pieces. Two styles are available:
segment-style: pill(default) — a rounded pill on the spine, label centred.segment-style: chevron— an arrow on the spine pointing along the row's direction (right on even rows, left on odd rows).
era (the translucent full-row backdrop from
horizontal) isn't supported on timeline-s.
Streams and junctions are
not supported — they assume a stream-graph that doesn't
translate to a wrap-and-snake flow. If a .tl file
contains them, the parser skips those lines and the preview shows
a visible amber warning so authors know.
width
A new config field, only meaningful on timeline-s. Three
forms:
width: N nodes— N node slots per row (e.g.width: 3 nodesfor a snake,width: 1 nodesfor a vertical spine).width: Npx— canvas width in pixels; the renderer fits as many nodes per row as the width permits.- Omitted — defaults to A4 portrait at 96 DPI (794px). The intended use case is "fit on a printed page".
Malformed width: values throw a parser error rather
than silently defaulting — typos surface earlier this way.
Supported configuration and named fields
Reuses the timeline syntax for nodes, descriptions, icons, avatars,
progress nodes, and groups. The supported config keys are:
title, width, node-size,
node-style, segment-style, spine,
description-side, label-size,
label-style (text / pill /
button), legend, font, and
background-colour. title draws a heading
above the diagram; legend draws a chip-row of the
groups used below it. Node-level named fields
(label, title, body,
group, icon, avatar,
progress, description-side) all behave as
in the horizontal timeline. description-side is
literal — above means above the node's row,
below means below — it does not flip with the row's
direction.
spine
spine: visible | hidden. Defaults to
visible, which draws the connecting line between
adjacent nodes (straight on intra-row, semicircle U-curves at
row breaks). spine: hidden suppresses the whole
spine, leaving the timeline-s as a clean grid of standalone
nodes — useful when combined with width: 3 nodes
and progress rings to lay out a project dashboard.
Auto-tightened row bands
If every node's description sits on the same side of the spine
(e.g. description-side: below with no per-node
overrides), the opposite half of each row band collapses out
and rows pack flush against the empty side. Mixed / alternate
layouts keep their full band, so nothing changes for the
common case.
Examples
timeline-s width: 3 nodes node-size: 2 node | n1 | 1 | label:Step 1 title:Initial consult group:blue node | n2 | 2 | label:Step 2 title:Design inquiry group:magenta node | n3 | 3 | label:Step 3 title:Initial design group:gold node | n4 | 4 | label:Step 4 title:Sales pitch group:violet node | n5 | 5 | label:Step 5 title:Purchasing group:green node | n6 | 6 | label:Step 6 title:Install group:blush
timeline-s width: 1 nodes node-size: 2 node | n1 | 1 | label:Backups title:Backups confirmed group:sky node | n2 | 2 | label:Approval title:CAB sign-off group:sky node | n3 | 3 | label:Staging title:Deploy to staging group:amber progress:100 node | n4 | 4 | label:Prod title:Deploy to prod group:green progress:60
Segments cover ranges of node values — default is pill:
timeline-s width: 3 nodes node-size: 2 segment-style: pill segment | discover | 1 | 3 | label:Discover group:sky segment | build | 4 | 6 | label:Build group:sage node | n1 | 1 | label:Step 1 title:Initial consult node | n2 | 2 | label:Step 2 title:Design inquiry node | n3 | 3 | label:Step 3 title:Initial design node | n4 | 4 | label:Step 4 title:Bids out node | n5 | 5 | label:Step 5 title:Sales pitch node | n6 | 6 | label:Step 6 title:Install
Chevron flips direction per row to follow the snake:
timeline-s width: 3 nodes node-size: 2 segment-style: chevron segment | discover | 1 | 3 | label:Discover group:sky segment | build | 4 | 6 | label:Build group:sage node | n1 | 1 | label:Step 1 title:Initial consult node | n2 | 2 | label:Step 2 title:Design inquiry node | n3 | 3 | label:Step 3 title:Initial design node | n4 | 4 | label:Step 4 title:Bids out node | n5 | 5 | label:Step 5 title:Sales pitch node | n6 | 6 | label:Step 6 title:Install
timeline-o
A separate diagram type — files opening with timeline-o
lay out nodes around a closed ring. Every value (node values
and segment endpoints) is mapped linearly onto the circle:
the smallest value goes to 12 o'clock, the largest to 12 o'clock as
well (the seam closes when they coincide there). Descriptions render
radially outward from each node. The ring radius grows automatically
so the largest description box still fits in the smallest angular
gap between any two adjacent values.
Use it for cyclical processes (sprints, seasons, retro loops, recurring rituals) — anything where the last step flows back into the first.
Numeric values are the default. To plot dates round the ring, add
the same declarations as on a regular timeline:
value-format: date with
date-format: YYYY / YYYYMMM /
YYYYMMMDD / YYYYMMMDD HH:MM. Segments still
take the same dated values for from and to;
they sweep the arc between those points directly, with or without a
node landing inside.
Supported configuration and named fields
Reuses the timeline syntax for nodes, descriptions, icons, avatars,
progress nodes, and groups. The supported config keys are:
title, node-size, node-style,
segment-style, label-size,
label-style (text / pill /
button), legend, font, and
background-colour. title draws a heading
above the ring; legend draws a chip-row of the groups
used below it. Node-level named fields
(label, title, body,
group, icon, avatar,
progress) all behave as elsewhere.
description-side is ignored on timeline-o — every
description sits outside the ring, regardless of what the field
says. Authors who try to push a description inward see a
console warning.
Segments render as arc bands along the ring, with two styles:
segment-style: pill(default) — a thick stroked arc with round caps. Label sits at the angular midpoint along the ring.segment-style: chevron— same arc with a filled triangle tip at the trailing (clockwise-later) end of the range, pointing in the ring's rotation direction.
Era segments aren't supported on timeline-o.
Streams and junctions aren't supported either — they assume a stream-graph that doesn't translate to a single ring. The parser skips them and the preview shows an amber warning.
Example
timeline-o node-size: 3 segment-style: pill segment | plan | 1 | 4 | label:Plan + Build group:sky segment | ship | 5 | 8 | label:Ship + Learn group:sage node | n1 | 1 | label:Kick-off title:Story review group:sky icon:flag node | n2 | 2 | label:Design title:Wireframes group:sky icon:lightbulb node | n3 | 3 | label:Build title:Implementation group:sky icon:zap node | n4 | 4 | label:Review title:PR walkthrough group:sky icon:check node | n5 | 5 | label:Ship title:Release group:sage icon:flag node | n6 | 6 | label:Monitor title:Watch dashboards group:sage icon:clock node | n7 | 7 | label:Demo title:Stakeholder demo group:sage icon:trophy node | n8 | 8 | label:Retro title:Review and adapt group:sage icon:users
radial-diagram
A diagram for aggregating many small records around a ring. Three levels of hierarchy:
section— an angular slice of the wheel (e.g. a decade, a war, a Games). Sections themselves carry no colour; the inner ring stays neutral grey and their angular slot is equal-sized by default.node— a grouping of entries inside a section. Each node's colour comes from astreamthat lists its ID, so the body of the file stays free of repeatedgroup:tokens. When nodes are visible, each gets a coloured sub-arc along the section's inner ring; usenode-style: hiddento drop those sub-arcs and use nodes purely as a colour grouping with stacked tiers.stream— a colour category that lists the node IDs it owns plus agroup:from the palette. Streams also populate the legend with their label andorder:, so the legend maintains itself.+ textdata entries — individual records under a node (or an implicit node if the section has no explicit nodes). Three modes:+ text= one labelled row;+ text | N= labelled magnitude bar N rows tall;+ N= N unlabelled rows (bulk count). Labelled rows render as plain default-colour curved text; bulk counts render as coloured arc bands in the node's group colour. Wrap numeric labels in quotes (+ "1945") so they aren't parsed as a count.
Every section gets the same angular slot by default; switch to
section-sizing: proportional if you want the arc
width to reflect record count. Streams (declared at the top of
the file) own the palette AND the legend, which builds itself
from their label + order: fields.
Opener
First line: radial-diagram.
Config
title:— optional diagram title. Rendered in the centre of the ring by default.title-position: centre | side—centredefault.sideputs the title in the panel alongside the legend.section-sizing: equal | proportional—equaldefault; every section gets the same angular slot.proportionalsizes each section by total entry count, useful when arc width is meant to read as "how much of the data sits in this bucket".section-order: declared | count-desc | count-asc | label—declareddefault.start-angle: N— degrees clockwise from 12 o'clock; default 0.legend: on | off | left | right—ondefault for this diagram type.node-style: circle | hidden—circledraws each node's coloured sub-arc on the section's inner ring;hiddendrops those arcs so nodes only carry colour groupings (and their titles render inline as curved headings).node-layout: adjacent | stacked—adjacent(default) gives each node its own angular sub-slot inside the section.stackedmakes every node share the section's full angular slot and stack radially as tiers, with each node's outer-cap arc acting as a divider between tiers.inner-radius: N— minimum radius for the section's inner ring (default 120). The outer ring sits atinner-radius + (tallest node's entry count) × row-step.inner-radius: 0activates rose-petal mode: the centre collapses to a single point and each section becomes a triangular wedge that fills outward from there.entry-row-step: N— radial distance per entry row, default 22.font/background-colour— same semantics as elsewhere.
section
section | id | label:sector label
label: renders on the outer rim along an arc.
Sections themselves carry no colour — the inner ring stays
neutral grey; nodes (via streams) are where colour lives.
node
node | id | label:node label
A node belongs to the most recently declared section.
Multiple nodes per section are fine. The node's colour is
supplied by whichever stream
lists its id; a node can override that with its own
group: token, but the recommended pattern is to
let streams own the palette and have plain
node | id | label:… lines in the body of the file.
label:, when set, renders as a curved heading
across the top edge of the node's tier (when nodes are
hidden); omit it for a clean bar.
stream
stream | id | label | group:palette colour order:N
Declares a colour category. Every node | … line
that follows the stream line belongs to it (until the next
stream | …, section | …, or top-level
config line closes the block) — indentation is optional. Each
member node inherits the stream's group: unless it
sets its own. Streams also populate the legend automatically —
the label is the legend label, and order:
controls legend position.
A radial-diagram file follows a fixed top-to-bottom
shape: config at the top,
all section | … declarations
next, then a stream | … block per colour
category, with each node inside the stream block
naming its section via section:id.
Data entries — + lines
Three modes, all attached to the most recently declared node (or an implicit one if the section has no explicit nodes):
+ text— one labelled row. Renders as a coloured arc band with the label curved on top.+ text | N— labelled magnitude. The entry occupies N rows worth of radial space, drawn as an N-row coloured bar, with the label on the outermost row.+ N— unlabelled bulk count. Renders as N coloured rows with no text. Wrap purely-numeric labels in quotes (+ "1945") so they aren't read as a count.
Entries within a node stack outward in listing order; rows
consume entry-row-step px each. Colour inherits
node → section unless an explicit group token is given on
the line (+ text | group).
A section's outer ring sits just past the tallest node's
stack, so magnitudes and counts visibly enlarge their
section.
+ lines are recognised but no-op on other diagram
types, so a .tl file can be ported between diagram
types without parse errors.
Legend
Streams populate the legend automatically. Declare one stream
per palette entry; order: controls the order they
appear in the side panel:
stream | conservative | Conservative | group:blue order:1 stream | labour | Labour | group:red order:2 stream | coalition | Coalition | group:gold order:3
The legacy legend | group | label:… order:…
lines are still parsed (useful for relabelling or re-ordering
a stream's entry after the fact), but most files don't need
them.
Undeclared groups still appear, appended after the declared ones.
Example
radial-diagram title: UK general elections 1945–2024 title-position: side # Sections — declared up front; nodes anchor to one via section:<id>. section | s1940s | label:1940s section | s1950s | label:1950s section | s1970s | label:1970s section | s2010s | label:2010s stream | conservative | Conservative | group:blue order:1 node | con50 | section:s1950s label:Conservative + 1951 + 1955 + 1959 node | con70a | section:s1970s label:Conservative + 1970 node | con70b | section:s1970s label:Conservative + 1979 node | con10 | section:s2010s label:Conservative + 2015 + 2017 + 2019 stream | labour | Labour | group:red order:2 node | lab40 | section:s1940s label:Labour + 1945 + 1950 node | lab70 | section:s1970s label:Labour + 1974 Feb + 1974 Oct stream | coalition | Coalition | group:gold order:3 node | coa10 | section:s2010s label:Coalition + 2010
Rose-petal mode (inner-radius: 0)
Setting inner-radius: 0 collapses the wheel's hollow
centre to a single point. Each section becomes a triangular wedge
that fills outward from there — Florence Nightingale's
Coxcomb / rose chart. Petal length is set by
(entry count) × row-step, so a section with more
entries gets a longer petal.
Three things change automatically in this mode:
- The section's inner-ring arc and the per-node sub-arcs are
suppressed — there's no inner radius for them to sit on.
node-style: hiddenisn't strictly required, but it's the natural pairing. title-position: centreis auto-routed toside(with a console warning), because a centre title would overlap where the petals meet. Iflegend: offis also set, the title sits above the wheel within the canvas margin.- Entry labels whose ring radius is below
row-step × 1.5switch from a curvedtextPathto straight horizontal text — at that radius the curved baseline doubles back on itself and reads as a knot.
The wheel itself is smaller than a normal radial-diagram because the 120 px inner hole is gone. The classic use is comparing counts across categorical buckets (months, departments, regions, species) — anywhere a bar chart would do but you want the "petals" framing.
radial-diagram title: Books finished — 2025 title-position: side legend: right node-style: hidden node-layout: stacked inner-radius: 0 entry-row-step: 18 # Sections — one per month, all declared up front. section | jan | label:January section | feb | label:February section | mar | label:March section | apr | label:April section | may | label:May section | jun | label:June section | jul | label:July section | aug | label:August section | sep | label:September section | oct | label:October section | nov | label:November section | dec | label:December stream | winter | Winter | group:sky order:1 node | w_jan | section:jan + 3 node | w_feb | section:feb + 2 node | w_mar | section:mar + 4 stream | spring | Spring | group:mint order:2 node | s_apr | section:apr + 3 node | s_may | section:may + 5 node | s_jun | section:jun + 3 stream | summer | Summer | group:gold order:3 node | u_jul | section:jul + 6 node | u_aug | section:aug + 5 node | u_sep | section:sep + 6 stream | autumn | Autumn | group:peach order:4 node | a_oct | section:oct + 4 node | a_nov | section:nov + 2 node | a_dec | section:dec + 5
family-tree
Descendant chart. Different grammar from the timeline family —
Node / Partner / Child
declarations with capitalised type names — and a layout engine
that centres each couple's children below the couple's midpoint
so the drop line lands on the centre of the sibling bar.
Opener and title
First line must be family-tree. An optional
title: config line gives the diagram a centred heading.
Node
Node | id | name | dateOrRef | birthYear | named-fields…
id— referenced byPartnerandChildlines.name— rendered below the circle, split at the last space into a two-line label.dateOrRef— free-text date string, e.g.b.1894 d.1972, rendered as a smaller line beneath the name.birthYear— four-digit year. Renders inside the node circle. Suppressed whenavatar:is set.- Named fields:
group:(palette colour),gen:(integer generation index, drives vertical row),avatar:(URL of a portrait clipped to the circle).
Partner
Partner | p1-id | p2-id | married-year —
joins two nodes as a couple. Rendered as a horizontal line
between the two circles, with the year as italic text above.
Child
Child | parent1-id | parent2-id | child-id — drops
a vertical line from the parents' midpoint down to a sibling
bar that spans all of the couple's children, then up to each
child's circle. The layout engine centres children below their
parents so the drop line lands on the centre of the sibling
bar — no manual ordering needed.
Supported config
title, node-size, node-style
(with hexagon swapping each node's circle for a flat-top
hexagon), legend, font and
background-colour. node-size is the 1–20
scale; legend draws a chip-row of the groups used below
the tree.
family-tree title: Example tree Node | a | Alice | b.1950 d.2020 | 1950 | group:rose Node | b | Bob | b.1948 d.2018 | 1948 | group:sky Node | c | Carol | b.1980 | 1980 | group:violet Node | d | Dan | b.1982 | 1982 | group:sage Partner | a | b | 1972 Child | a | b | c Child | a | b | d
org-chart
Organisational hierarchy. Built for HR-system data: every person
is one Node line that names its own id, the person's
name, and the id of their manager. A pure tree — exactly one
manager per person — so there are no separate relationship lines
to keep in sync.
Opener and title
First line must be org-chart. An optional
title: config line gives the diagram a centred
heading.
Node
Node | id | name | manager-id | named-fields…
id— uniquely identifies the person. An email address works well, since HR exports already carry one, but any string with no|or spaces is accepted.name— rendered below the circle, split at the last space into a two-line label.manager-id— theidof this person's manager. Leave it blank for the person at the top. An id that matches no node is treated as a root, and the builder shows a warning.- Named fields:
label:(job title, shown under the name),group:(palette colour — see below),avatar:(URL of a portrait clipped to the circle),body:(notes).
Colour inheritance
Org charts are the one diagram type where group:
cascades. Set group: on a manager and every report
beneath them — to any depth — borrows that colour. A descendant
that sets its own group: overrides the cascade from
that point down its branch. A node with no group:
anywhere up its management chain falls back to the neutral
grey group. This means an entire division can be
coloured by setting one field on its VP.
Supported config
title, node-size, node-style
(circle / hexagon), legend,
font and background-colour behave the
same as on the other diagram types. legend draws a
chip-row of the groups used below the chart.
org-chart title: Example org Node | ceo | Ada Lin | | label:CEO group:navy Node | eng | Ben Park | ceo | label:VP Engineering Node | sales| Cleo Mensah | ceo | label:VP Sales group:gold Node | dev1 | Dana Roy | eng | label:Engineer Node | dev2 | Eli Stone | eng | label:Engineer Node | rep1 | Faye Ortiz | sales| label:Account Exec