How to make a diagram¶
Boxes, groups and labelled arrows. This is the recipe behind examples/diagrams/, which ports four real Mermaid diagrams.
Is svg-plus the right tool for this one?
Only if you know the arrangement. In an architecture diagram the layout usually carries the argument — these three sit in a tier, that one spans the others — and an engine will move them. If the shape comes from data and varies in size, use Graphviz or D2 and embed the SVG.
1. Decide the tiers first¶
A diagram is a Stack of tiers; a tier is a Row of boxes. Say that first and the rest follows.
from svg_plus import Doc, Row
doc = Doc(900, pad=16).add(
Row(edge_node, cdn_node, gap=22.0), # a tier
Row(gateway_node, workers_node, gap=22.0), # the next one
)
This is where svg-plus earns its place. Mermaid has nowhere to put the ordering, so people force it with invisible edges — three of your five diagrams do. Row(a, b, c) states it once and nothing competes with it.
2. Build a node helper¶
Every box in a diagram looks the same, so write it once. What it is in bold, what it is made of underneath:
from svg_plus import Frame, Stack, Text
def node(label, detail="", *, key=None):
lines = [Text(label, size=11.0, weight=700)]
lines += [Text(line, size=9.0, fill="muted") for line in detail.split("\n") if line]
return Frame(Stack(*lines, gap=3.0), pad=9.0, key=key)
Splitting on \n matters: a label is not prose, so a newline in it is meant. A plain Text would fold it into a paragraph.
3. Group with tinted enclosures¶
A Frame around a Stack is Mermaid's subgraph:
from svg_plus import tint
def group(title, *children, color="#1565c0", key=None):
return Frame(
Stack(
Text(title.upper(), size=8.5, weight=700, fill=color, tracking=0.9, wrap=False),
*children,
gap=9.0,
),
fill=tint(color, 0.965),
stroke=color,
radius=8.0,
key=key,
)
Groups nest, and a group can be connected to like any other block — give it a key.
4. Name the boxes, then join them¶
Names resolve after the layout has run, so an arrow follows its boxes when they move:
heads takes ->, <-, <-> or --.
Connect at the right level¶
If an edge is really about two groups, connect the groups. Joining a box in one to a box in another sends a line back across everything between:
Routing¶
You get this without asking:
| Boxes | Route |
|---|---|
| Line up on an axis | A straight arrow down the middle of what they share |
| Line up on neither | Square corners, rounded, turning in the gutter |
| Nearly touching | Clearance is given up so the arrow still spans the gap |
For an edge running back against the flow, name the side it should travel:
Without via, a route uses its two endpoints only — so a back-edge across three tiers will cross them. Leave the figure enough pad for the lane to run in.
5. Know where a label fits¶
A label sits beside its line, never across it. The constraint is the gutter:
- Vertical runs — labels sit to the side, where there is width to spare. These are your good ones.
- Horizontal runs between side-by-side boxes — the gutter is usually narrower than the word, and the label will overlap. Either widen the gap, or fold the information into the box.
In a tier diagram, that means: label the tier-to-tier edges, not the within-tier ones.
6. Add a stamp if you need one¶
An overlay sits on top of the flow, against a block that has already landed:
Nine anchor points, from top-left to bottom-right. Put it on a corner no arrow arrives at.
7. Something the blocks cannot express¶
A decision diamond, a lifeline, a swimlane — reserve the height and draw it:
from svg_plus import Draw, Rect
def diamond(label, *, key=None):
def paint(canvas, box):
points = [(box.cx, box.y), (box.right, box.cy), (box.cx, box.bottom), (box.x, box.cy)]
moves = " ".join(f"{'M' if i == 0 else 'L'} {x:.1f} {y:.1f}" for i, (x, y) in enumerate(points))
canvas.path(f"{moves} Z", fill="#fff8e1", stroke="#f9a825")
canvas.text(box.cx, box.cy + 3.5, label, canvas.theme.style(10.0, weight=700), "middle")
return Draw(60.0, paint, key=key)
examples/diagrams/smo_request.py takes this further: a whole sequence diagram — participants across the top, lifelines, nested activation bars — is one Block in about seventy lines, because that layout is computable. svg-plus has no sequence diagram and did not need one.
Checklist¶
- Tiers are rows; the flow is the stack. No invisible edges.
- One
node()helper, onegroup()helper, used everywhere. - Edges join things at the same level of abstraction.
- Labels are on the tier-to-tier edges, where there is room.
- Back-edges name a
viaside. -
doc.describe(...)gives the figure an accessible name.