Project: Drawing Diagrams with SVG
Many teams draw their architecture with a text description and a tool
that turns it into a picture: Graphviz reads a -> b, Mermaid reads
graph TD. The description is small, it can be reviewed in a diff, and
nobody moves boxes by hand. In this project we'll build a small tool of
that kind from the parts of this book. The description is XML, the
layout is computed by XQuery, and the picture is SVG, which is XML too,
so the same tools can write it, check it and change it afterwards.
The diagram we'll draw is the Vane system and who calls whom in it: the two SDKs, the modules of the Vane service, and the stores the service keeps its data in. Later projects draw other things with the same code: the flag's state machine, a diff of call chains, and a diff of traces.
The files are in docs/book/examples/svg.
SVG in one page
SVG (Scalable Vector Graphics) is an XML vocabulary for drawings. Its
elements are in the namespace http://www.w3.org/2000/svg; without that
namespace, a browser shows an SVG file as unknown XML instead of a
picture. Here is one box with a label and an arrow:
box.svg:
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="140" viewBox="0 0 200 140">
<style>
rect { fill: #eef3fb; stroke: #3b5b92 }
</style>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5"
markerWidth="8" markerHeight="8" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z"/>
</marker>
</defs>
<rect x="30" y="20" width="140" height="40" rx="6"/>
<text x="100" y="45" text-anchor="middle">vane/flags</text>
<line x1="100" y1="60" x2="100" y2="120" stroke="black" marker-end="url(#arrow)"/>
</svg>
The root svg element gives the size of the picture (width, height)
and its coordinate system (viewBox: the left, top, width and height of
the area drawn). Coordinates grow to the right and down: (0, 0) is the
top left corner. The other elements draw in document order, later ones
on top of earlier ones:
rectis a box atx,yof sizewidthbyheight;rxrounds its corners.textwrites text with its baseline aty;text-anchor="middle"centres it onx.linegoes from (x1,y1) to (x2,y2).marker-endputs a marker on its end: here the triangle thatmarkerdefines insidedefs, which holds things that are used rather than drawn, and is referred to by itsidasurl(#arrow).styleholds CSS rules, as in HTML. Presentation can also be written as attributes (stroke="black"); a rule instyleapplies to every element it matches.g(not used here) groups elements; a class or an id on the group applies to everything inside.
Open the file in a browser to see it. For the tools in this book it is just a document:
$ sxml check box.svg
$ sxml xpath --context-item=box.svg '/*/* ! local-name()'
"style"
"defs"
"rect"
"text"
"line"
The description and the drawing
A diagram is described by a small vocabulary of our own, without a
namespace: a diagram with a title, a node for each box (an id,
a label, and kind="store" for a data store), and an edge for each
arrow, from the caller to what it calls:
diagram.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!-- The Vane system and who calls whom: the two SDKs, the modules of the
Vane service (one program; a module is a box named vane/MODULE), and
the stores it keeps its data in. A node is a box; kind="store" draws
a data store. An edge goes from the caller to what it calls. The
layout is computed: diagram.svg is the drawing. -->
<diagram title="Vane">
<node id="browser" label="@vane/browser"/>
<node id="node" label="@vane/node"/>
<node id="api" label="vane/api"/>
<node id="flags" label="vane/flags"/>
<node id="registry" label="vane/registry"/>
<node id="cache" label="vane/cache"/>
<node id="postgresql" label="postgresql" kind="store"/>
<node id="redis" label="redis" kind="store"/>
<edge from="browser" to="api" label="HTTP"/>
<edge from="node" to="api" label="HTTP"/>
<edge from="api" to="flags"/>
<edge from="flags" to="registry"/>
<edge from="flags" to="cache"/>
<edge from="registry" to="postgresql" label="SQL"/>
<edge from="cache" to="redis" label="Redis"/>
</diagram>
The Vane service is one program, so its parts are modules, not
services; each module is a box of its own, labelled vane/MODULE. The
prefix says that the boxes are parts of one program. A box drawn around
them would say it too, but it would need a second kind of node and a
layout that nests boxes inside boxes, new machinery for one picture; the
labels do the job with what the vocabulary already has. The two SDKs
call the service over HTTP, the modules call each other directly (those
edges have no label), and two modules talk to the stores.
Nothing in the description says where a box goes. That is the tool's job, and its result is this drawing:
diagram.svg:
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="480" viewBox="0 0 360 480">
<title>Vane</title>
<style>
.node rect { fill: #eef3fb; stroke: #3b5b92; stroke-width: 1.5 }
.store rect { fill: #f6efe3; stroke: #8a6a2f }
.node text { font: 14px sans-serif; fill: #1b1b1b }
.edge line { stroke: #555555; stroke-width: 1.5 }
.edge text { font: 12px sans-serif; fill: #555555 }
</style>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555555"/>
</marker>
</defs>
<g class="edge" id="edge-browser-api">
<line x1="90" y1="60" x2="180" y2="120" marker-end="url(#arrow)"/>
<text x="141" y="94">HTTP</text>
</g>
<g class="edge" id="edge-node-api">
<line x1="270" y1="60" x2="180" y2="120" marker-end="url(#arrow)"/>
<text x="231" y="94">HTTP</text>
</g>
<g class="edge" id="edge-api-flags">
<line x1="180" y1="160" x2="180" y2="220" marker-end="url(#arrow)"/>
</g>
<g class="edge" id="edge-flags-registry">
<line x1="180" y1="260" x2="90" y2="320" marker-end="url(#arrow)"/>
</g>
<g class="edge" id="edge-flags-cache">
<line x1="180" y1="260" x2="270" y2="320" marker-end="url(#arrow)"/>
</g>
<g class="edge" id="edge-registry-postgresql">
<line x1="90" y1="360" x2="90" y2="420" marker-end="url(#arrow)"/>
<text x="96" y="394">SQL</text>
</g>
<g class="edge" id="edge-cache-redis">
<line x1="270" y1="360" x2="270" y2="420" marker-end="url(#arrow)"/>
<text x="276" y="394">Redis</text>
</g>
<g class="node service" id="node-browser">
<rect x="20" y="20" width="140" height="40" rx="6"/>
<text x="90" y="45" text-anchor="middle">@vane/browser</text>
</g>
<g class="node service" id="node-node">
<rect x="200" y="20" width="140" height="40" rx="6"/>
<text x="270" y="45" text-anchor="middle">@vane/node</text>
</g>
<g class="node service" id="node-api">
<rect x="110" y="120" width="140" height="40" rx="6"/>
<text x="180" y="145" text-anchor="middle">vane/api</text>
</g>
<g class="node service" id="node-flags">
<rect x="110" y="220" width="140" height="40" rx="6"/>
<text x="180" y="245" text-anchor="middle">vane/flags</text>
</g>
<g class="node service" id="node-registry">
<rect x="20" y="320" width="140" height="40" rx="6"/>
<text x="90" y="345" text-anchor="middle">vane/registry</text>
</g>
<g class="node service" id="node-cache">
<rect x="200" y="320" width="140" height="40" rx="6"/>
<text x="270" y="345" text-anchor="middle">vane/cache</text>
</g>
<g class="node store" id="node-postgresql">
<rect x="20" y="420" width="140" height="40" rx="20"/>
<text x="90" y="445" text-anchor="middle">postgresql</text>
</g>
<g class="node store" id="node-redis">
<rect x="200" y="420" width="140" height="40" rx="20"/>
<text x="270" y="445" text-anchor="middle">redis</text>
</g>
</svg>
The boxes are in five rows: the two SDKs at the top, the api module
they call below them, and so on down to PostgreSQL and Redis. Each node
is a group g with a rect and a text; each edge is a group with a
line and, when the edge has one, its label at the middle. The groups
have ids made from the description (node-registry, edge-flags-cache)
and classes (node, service, store, edge) that the five style
rules use: service is the class of every box that is not a store, an
SDK or a module alike. Stores have rounder corners.
Both files are ordinary XML:
$ sxml check diagram.xml diagram.svg
$ sxml xpath --context-item=diagram.xml 'count(//node), count(//edge), //node[@kind = "store"]/@label/string()'
8
7
"postgresql"
"redis"
An XPath expression over the drawing has to say the namespace. A name
test without a prefix, such as //rect, looks for elements in no
namespace and finds none. *:rect matches rect in any namespace, and
Q{http://www.w3.org/2000/svg}g names the namespace in the expression
itself:
$ sxml xpath --context-item=diagram.svg 'count(//rect), count(//*:rect)'
0
8
$ sxml xpath --context-item=diagram.svg '//Q{http://www.w3.org/2000/svg}g/@id/string()'
"edge-browser-api"
"edge-node-api"
"edge-api-flags"
"edge-flags-registry"
"edge-flags-cache"
"edge-registry-postgresql"
"edge-cache-redis"
"node-browser"
"node-node"
"node-api"
"node-flags"
"node-registry"
"node-cache"
"node-postgresql"
"node-redis"
The plan
The tool has two halves, and the next sections build them one at a time:
- Computing a Layout decides where each box goes: a row for each step away from the top, boxes centred in their row. This is a small graph algorithm, written with maps, arrays, records and functions as values.
- Writing SVG with XQuery turns the layout into
diagram.svgwith constructors in the SVG namespace, and packs both halves into a library module,draw.xqm, that the later projects import. - Drawing with XSLT draws the same diagram with template rules, from a description the layout query has given coordinates.
- Transforming SVG changes drawings that already exist, ours or anyone's: a dark theme, one node highlighted, ids and editor metadata removed, the text listed, the size changed.