Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  • rect is a box at x, y of size width by height; rx rounds its corners.
  • text writes text with its baseline at y; text-anchor="middle" centres it on x.
  • line goes from (x1, y1) to (x2, y2). marker-end puts a marker on its end: here the triangle that marker defines inside defs, which holds things that are used rather than drawn, and is referred to by its id as url(#arrow).
  • style holds CSS rules, as in HTML. Presentation can also be written as attributes (stroke="black"); a rule in style applies 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.svg with 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.