← JSHaworth · Timeline Builder · Syntax
← Back to builder

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:

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

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.

spacing: even
spacing: scale

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-size: 1
node-size: 4

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.

node-style: circle
node-style: hexagon
node-style: hidden
hexagon + icon

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-position: centre
axis-position: hide

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.

segment-style: pill
segment-style: era
segment-style: chevron

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.

stream-style: line
stream-style: stream

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.

description-side: above
description-side: alternate

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:

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.

connector-style: lollipop
connector-style: title-underline

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.

solid-dark + rounded
invisible + circle

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.

legend: left
legend: below

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.

Sand crosses green to reach blue
Sand sits next to blue — clean merge

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.

Antipattern — duplicate terminal nodes
Recommended — one shared junction node

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.

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").

FieldApplies toEffect
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: 120 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: 120 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.

Examples: title-style: inherit, title-style: all-caps bold, title-style: #2D6CDF all-caps, title-style: red bold.

label-size

label-size: 120 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:

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.

Browse all Lucide icons → 1500+ free, ISC-licensed icons

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:

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:

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:

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:

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:

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:

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:

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

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):

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 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…

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…

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