SVG export
meltemi-diagrams export <file.meltemi> renders every flow and free diagram of a file to SVG. This page describes what
the exporter draws, how it chooses positions it was not given, and what the SVG contains. The
pipeline is compile() in @meltemi/diagrams-model, layoutDiagram() in @meltemi/diagrams-layout (model to vector scene)
and renderSvg() in @meltemi/diagrams-render (scene to text).
pnpm mdg export examples/purchase/diagrams/architecture.meltemiOptions: --diagram <id> exports one diagram, --out <path> names the file (one diagram) or an
existing directory, --stdout prints one diagram, --format svg is the only format today. The
output is named <file>.svg, or <file>.<diagram-id>.svg when the file holds several diagrams.
With --stdout, diagnostics go to stderr so that stdout holds only the SVG. Exit codes follow
meltemi-diagrams validate: 0, 1 with error diagnostics, 2 for usage errors.
What renders
Section titled “What renders”Every flow, free, sequence and C4 diagram without errors. The exporter arranges layout auto and layout assisted
diagrams with ELK (layout.md); layout manual reports every element without a
place as layout.missing-placement. Hosts that call layoutDiagram() without an engine render
auto and assisted diagrams only when every element is placed, and report
layout.mode-unsupported otherwise. A file without a renderable diagram reports
export.no-diagram. Free diagrams, their objects, images and layers are described in
free.md. Sequence diagrams are laid out without an engine from the order of their
events, and --scenario <id> exports one path (sequence.md). C4 views are laid
out like flow diagrams, with C4 shapes (c4.md).
Groups never need a place: without place and size a group fits its children with the theme’s
groupPadding on the sides and bottom (16 px in technical.*) and its groupHeader on top for
the title (40 px), or groupPadding again when it is untitled (label "", no description, no
icon). With both statements it is drawn exactly there. One statement without the other is
layout.missing-placement.
In flow diagrams, coordinates are diagram coordinates for every element, inside groups too. In free
diagrams, the children of a group are placed relative to its interior (free.md).
place is the centre of the box and size its width and height.
Default sizes
Section titled “Default sizes”Sizes come from the theme’s tokens (styles.md). The values below are those of the
technical.* themes; other type sizes scale them.
| Kind | Without size |
|---|---|
node |
160 x 56, wider when the label or a description line needs it (+padding, 16 px, on each side), and wider or taller for an icon beside, above or below the text at its iconSize |
node with shape |
the shape’s size (free.md), grown until the text fits the shape’s text area; shape icon is 64 x 64, or a written iconSize, plus its frame, with its label outside |
node, decision, data with iconPosition center |
120 x 120, widened for its label; a decision or a data store grows until its outline holds the label and the icon |
decision |
160 x 80, grown until the text fits inside the diamond |
data |
160 x 64, widened as a node |
group |
children’s box + groupPadding sides and bottom, groupHeader top (groupPadding when untitled), widened for its title; a child drawn with shape icon counts with its label |
tag |
measured text width + 18, by 18 |
note |
measured width of the longest line, by 18 per line plus 4 |
sticky |
160 x 160, never grown: its text wraps and shrinks to fit (free.md) |
pen |
the bounding box of its points, grown by half its width (3 by default) on every side (free.md) |
The base boxes scale with labelSize (160 x 56 at 13 px). A box is also at least as tall as its
label and description lines plus four fifths of padding above and below, so several description
lines grow it.
Text is measured with the bundled fonts before layout (see fonts.md), so boxes without
size always fit their text. An explicit size that leaves less than 6 px between the text and
an edge (for a diamond or another shape, the edge of its text area) is reported as
layout.text-overflow, with a hint giving a width that fits; the text is never scaled down.
Relation label pills are as wide as their measured text plus 12 px.
Shapes and text
Section titled “Shapes and text”| Kind | Drawing (sizes of technical.*) |
|---|---|
node |
rounded rectangle (radius 8, or its own radius); label labelSize 13 px at labelWeight 600; description lines 11 px; with shape, that shape, with the text in its text area |
data |
cylinder (body and top ellipse); text as a node, shifted down by half the ellipse height |
decision |
diamond; text as a node, inside the diamond |
group |
rounded band (groupRadius 14, or its own radius) with the title (groupTitleSize 13 px) at the top-left, description under it; an untitled group draws its outline only |
note |
left-aligned noteSize 11 px muted text, 18 px per line, no border unless fill, stroke or pattern is set |
tag |
pill with tagSize 10.5 px mono text; tag fill and tagText text by default |
sticky |
a soft shadow 3 px under a card of radius 2 in the theme’s sticky colour, its text wrapped and shrunk to fit, 15.6 px at most, in stickyText, centred |
pen |
one path through the midpoints of its points, round caps and joins, no fill, 3 px in the theme’s ink by default |
The twenty-four shapes that nodes and free boxes take, with their sizes, text areas and outlines,
are listed in free.md; shape icon draws the icon alone with the label
outside (free.md), and iconPosition places an icon beside, above,
below or at the centre of the text (styles.md).
Outlines are strokeWidth wide (1 px). The label and the description of a node, a decision, a
data store, a box or a C4 entity may hold \n line breaks, each line drawn under the last.
Relation labels are pills (surface fill, line stroke) with relationLabelSize 11 px text, centred
on the route point given by label <relation> at <n>% offset (dx, dy), or at 50 % without offset
when there is no label statement.
Connectors
Section titled “Connectors”A relation is a connectorWidth polyline (1.3 px in technical.*) ending in the theme’s
arrowhead, arrowSize long and seven eighths as wide:
triangle(technical and stage): a filled triangle; the line stops at its base;open(editorial): an unfilled chevron in the connector colour and width; the line reaches the tip;none: no head; the line reaches the box.
arrowhead and arrowtail styles choose any of the heads of styles.md
at the target and at the source; a two-way relation (<->, <-->) draws the target’s head at
both ends. A line (--, ---) draws no head at either end, whatever the theme’s arrowhead, and
reaches both boxes; a written arrowhead or arrowtail still draws, so a -- b with
arrowhead circle ends in a circle. A free line is drawn by the same code from its points, with
no head unless one is written (free.md). Heads are drawn after the line and before the label, in the connector’s colour and
width; filled shapes stop the line at their base, hollow ones at their back. A Bézier connector
is trimmed at both ends.
-->, --- and line dashed use a 5 4 dash pattern; line dotted draws round dots, a dash of 0 and
a gap of 1.5 × width + 3 with round caps. A dashed pen, whose caps are round, scales its pattern
with its width (a dash of max(5, 2 × width) and a gap of max(4, width) + width), so the gaps stay
open however wide it is.
The anchor on each box follows decision 0006:
- With waypoints, each side anchors at the projection of its neighbouring waypoint, clamped to the
side.
route ext from clients.east to gateway.north via [(1035, 48)]leavesclientsat y = 48 and entersgatewayat x = 1035. - Facing parallel sides without a waypoint between them meet at the centre of the overlap of their extents. A box spanning x 260..460 above a box spanning x 240..700 gets a vertical connector at x = 360. When the extents do not overlap, each side uses its own centre.
- Sides that do not face each other leave from their centres; perpendicular sides are joined by one elbow.
- An
autoport (the default, alsofrom apiwithout a port) picks the side that faces the neighbouring point, judged relative to the box proportions, so a wide band prefers its east or west side for a box beside it.
Explicit waypoints always win over these rules. A named port, or a compass side with a percentage
(api.east 50%), anchors at its own point, which waypoints do not move, and free ends that meet
one side of a box less than 16 px apart take lanes along it (layout.md). The routing property chooses
orthogonal, straight or curved connectors; see layout.md.
Anchors on outlines. These rules find points on a box’s rectangle. On a box drawn with
another shape (a decision’s diamond, a data store’s cylinder, a tag’s pill, a node or box with a
shape other than rounded, rectangle, square or framed), every such point, the engine’s
route ends included, then slides along the inward normal of its side to the first point of the
outline, or to the nearest outline point when the normal meets none; a point in a corner of the
rectangle (a lane pushed to the end of its side) goes to the nearest outline point. Arrowheads and curves follow
the final tip, and the text keeps the side and the percentage. A route that ends to d.north 5% on
a 160 × 80 diamond centred at (280, 200) ends at (208, 196), on its upper-left edge, not at the
rectangle’s corner.
Styles
Section titled “Styles”| Property | Element | Relation |
|---|---|---|
fill |
background; "none" draws no background |
label pill background |
stroke |
outline colour | connector and arrowhead colour |
color |
label and description text | label pill text |
line |
dashed or dotted outline |
overrides the arrow style (->, -->) |
width |
outline width | connector width |
radius |
corner radius of rounded rectangles and groups | not allowed |
shape |
the drawing of a node or a free box | not allowed |
pattern |
hatch, dots or cross over the shape, at 35 % opacity |
not allowed |
arrowhead |
not allowed | the head at the target |
arrowtail |
not allowed | the head at the source |
corners |
not allowed | rounded bends |
icon |
an icon left of or above the text (assets.md) | an icon in the label pill |
iconColor |
colour of a monochrome icon | colour of a monochrome icon |
Colours may be token names (stroke danger), which follow the theme. Patterns are drawn in the
element’s stroke colour, or muted without one, so meaning never depends on colour alone. Classes,
kind rules and the order in which they apply are in styles.md.
A label pill without stroke borrows the fill colour, so style r1 { fill "#1b1f23"; color "#fff" }
gives a dark circle for a one-character label. Styles with several targets
(style a, b, c { ... }) are the usual way to mark every proposed piece line dashed.
Themes
Section titled “Themes”The theme decides palette, type, spacing, strokes, arrowheads and the brand frame. Built-in themes:
| Name | Background | Surface | Ink | Muted | Line | Font | Frame |
|---|---|---|---|---|---|---|---|
technical.light (default) |
#F7FBFF |
#FFFFFF |
#0B1830 |
#526579 |
#DCE5ED |
Inter | footer |
technical.dark |
#07111F |
#0F1B2D |
#F7FBFF |
#88A0B9 |
#183149 |
Inter | footer |
editorial.light |
#ffffff |
#ffffff |
#1b1f23 |
#5b6570 |
#c7cdd2 |
IBM Plex Sans | none |
editorial.dark |
#13161a |
#1b1f24 |
#e5e8eb |
#9aa4ad |
#3b434b |
IBM Plex Sans | none |
stage.light |
#FFFFFF |
#FFFFFF |
#0A0F1A |
#3D4A5C |
#9AA8B8 |
Inter | header |
stage.dark |
#05080F |
#0E1626 |
#FFFFFF |
#A9B8CC |
#2B3B52 |
Inter | header |
sketch.light |
#F7FBFF |
#FFFFFF |
#0B1830 |
#526579 |
#DCE5ED |
Inter | footer |
sketch.dark |
#07111F |
#0F1B2D |
#F7FBFF |
#88A0B9 |
#183149 |
Inter | footer |
Line outlines the boxes of flow, free and C4 diagrams; sequence diagrams draw their heads, bars,
notes and lifelines in Muted instead (sequence.md).
- Technical, for architecture review: Aegean blue tags (
#2563EB, cyan in the dark theme), the Meltemi gradient for the mark (#1D4ED8to#0891B2on light,#3B82F6to#22D3EEon dark) and the sizes of the tables above. - Editorial, for documents and long reading: quiet greys, ink-coloured tags, smaller radii, 1 px connectors with open arrowheads and no frame.
- Stage, for presentations and video: higher contrast, 16 px labels, 13 px descriptions and relation labels, 2 px connectors, 10 px arrowheads, 48 px margins and a header frame.
- Sketch, for whiteboards and early thinking: the technical palettes, sizes and frame, with
rounded bends (
corners rounded) and strokes painted by hand (sketch light, below). Their text is in Patrick Hand, a hand font, with Inter for the characters it lacks.
Every built-in theme has the signal colours success, warning, danger and info, and the
built-in classes (styles.md).
A file can define its own theme with a theme <id> { } block (tokens in
grammar-0.2.md) and select it with theme "<id>". A project can set
a default theme in project.json (projects.md). meltemi-diagrams export --theme <name> overrides the diagram’s choice. Any unknown name renders with technical.light and reports
layout.unknown-theme as a warning. Explicit colours in styles are kept as written in every theme;
colour token names follow it.
frame footer (the default of the technical themes) adds a strip under the sheet with a hairline
in the line colour, the Meltemi mark drawn with the gradient accent to accentEnd, the brand
text (semibold) and the diagram title right-aligned. Its measures follow frameSize: at 12 px the
strip is 44 px high, the mark 24 px and the title 11 px. frame header (the stage themes) puts the
strip on top. mark none drops the symbol; brand "" drops the wordmark; frame none removes the
strip, which is how the editorial themes behave. The strip spans the whole canvas, is a scene group
with role frame, outside the diagram’s elements, and the canvas grows to fit the wordmark and
title. The editor’s exports can also leave the strip out by request, whatever the theme says,
keeping every other token; the hosted page offers it to the accounts an administrator entitled
(the web app).
Sketched strokes
Section titled “Sketched strokes”When the theme’s sketch is light or rough (the sketch.* themes are light), renderSvg
paints every stroked rectangle, path and polyline of the element, relation, label, frame, lifeline
and event groups, heads and label pills included, in three items:
- the fill alone, clean, with
stroke="none"(when the shape has a fill); - two stroke-only
<path fill="none">passes over it, at the item’s width, dash pattern, dash offset, class, opacity and transform. A rectangle is first turned into four sides joined by quarter arcs for its radius; the outline is then sampled every 16 px (light) or 12 px (rough), and each sample is moved along the outline’s normal by at most 0.6 px or 1.4 px.
The offsets come from a pseudo-random sequence seeded by the group’s id and the pass, so the same
text always renders the same bytes, and an edit elsewhere leaves a shape’s wobble alone. An
anonymous relation’s key (a->b#1) and a generated id change when statements are reordered, and
their wobble changes with them; name what must stay put. Text, images, icons, the brand mark,
gradients and patterns are untouched, and fills stay clean. Pen strokes are painted as written,
in one clean path: they are drawn by hand already (free.md). A sticky’s
card has no stroke unless one is styled, so only its styled outline is sketched. Only the
painting changes: the layout’s boxes, tracks and bounds, hit tests, the canvas’s selection
outlines and handles and the text editor use the clean geometry. Every export (SVG, PNG, PDF, the
players, video) and the canvas draw through renderSvg, so they all show the same strokes; a
moving dash moves on both passes. A sketched picture is larger: the whiteboard of the gallery is
6.5 KB clean and 35.6 KB sketched with --fonts system (decision 0058).
Presets and canvas
Section titled “Presets and canvas”preset document warns (layout.too-large) when the canvas is wider than 1,200 px. preset slide
extends the canvas to 16:9 with the content centred and warns when the content exceeds
1,920 × 1,080 px. preset board keeps the canvas as computed, whatever its size, without a
warning: a whiteboard grows with what is drawn on it. canvas (w, h) fixes the canvas and centres
the content; a dimension the content exceeds grows, with the same warning. Text is never shrunk:
split dense diagrams into views.
The SVG
Section titled “The SVG”- One
<svg>withxmlns,width,heightandviewBox; the canvas is the bounding box of the scene plus the thememargin(20 px intechnical.*), rounded up to whole pixels, then sized by the preset orcanvas. <title>and<desc>carry the diagramtitleanddescriptionwhen present.- A background rectangle in the theme background colour, another colour, or none with
--background transparent(export.md). - Elements with a
urldraw inside an<a href>in their group; a picturelinkwraps everything drawn in one<a href>. - Every element and relation is a
<g data-id="..." data-role="element|relation">; relation labels are a nested<g data-role="label">. A free line is an element group whose label is nested the same way. Editors can map clicks back to ids with these attributes. A group on a layer other thanmainalso carriesdata-layer="<name>", and an element or a group styled withopacityanopacityattribute. A pen is an element group whose stroke is one<path>with round caps and joins, followed by its heads; a sticky whose text does not fit holds a<clipPath>of its card for its text. - Images of free diagrams are
<image href="data:…" preserveAspectRatio="xMidYMid meet">inside their element group, with the description as a<title>child. No file or URL is referenced. - One
<defs>block appears when the scene uses the brand mark (<linearGradient>), patterns (<pattern>tiles referenced asfill="url(#…)"by an overlay shape) or embeds fonts (<style>with@font-facerules). The mark group carries atransform. Presentation is otherwise inline (fill,stroke,stroke-dasharray,font-size,font-weight): no CSS variables, no<marker>, nodominant-baseline, no external fonts. - Baselines come from the layout: the text centre plus half the cap height of the measured font, so text sits optically centred with any font.
- Fonts follow the export’s font policy:
embedded(default formeltemi-diagrams export: the bundled subset files as data URIs),outlined(text as paths witharia-label), orsystem(font names only; the first installed font is used). See fonts.md. - Rendering the same source twice gives byte-identical files, which is what the snapshot tests
under
corpus/andexamples/check. - Effects (
animate) are not drawn by default.--at <time>draws the picture at that instant, with particles, dashes and halos as ordinary shapes in a last<g data-role="motion">.--motion animatedwrites the animated profile: one<style>with CSS keyframes, classes on the animated shapes, and overlays that are invisible outside their animations, so the static picture remains the fallback (motion.md).
PNG and PDF are drawn from this SVG by the installed browser (export.md).