Developer guide¶
For working on svg-plus, or extending it from outside.
-
The three passes, the module layout, and the two decisions everything else follows from.
-
A block is two methods. The contract, the pitfalls, and three worked examples from the real figures.
Getting set up¶
make test # pytest
make lint # ruff, ruff format --check, ty, pyrefly, mypy
make format # ruff format + ruff check --fix
make figures # rebuild every example figure into examples/out/
make test-cov # coverage, currently 98 %
How the repository is laid out¶
src/svg_plus/ the library, seven modules
tests/
a_unit/ fast, isolated
b_integration/ Doc end to end, svglib round trip
c_e2e/ every example script, built for real
examples/
eurostack/ eight book figures (book.py holds the shared settings)
hop3/ four marketing posters (brand.py)
manual/ two figures documenting svg-plus (sheet.py)
docs/ this site
Each example family is one script per figure plus one settings module holding its fonts, palette and page format. A figure module exposes build() -> Doc and an OUTPUT filename, which is all the e2e suite needs to discover and build it.
House rules¶
No linter suppressions. No # noqa, no # type: ignore. If a rule fires, the code changes. Five checkers run in CI and all of them are clean.
Tests assert state, not calls. There are no mocks anywhere. The SVG is the product, so tests parse the output and assert on geometry — that a bar chart's readout does not run past its box, that a poster's content fits its page, that no line exceeds its measure.
Deliberate shortcuts are marked. A ponytail: comment names a simplification and its ceiling, so it can be found later:
Docstrings say why, not what. The signature already says what. A docstring earns its place by explaining a decision — why measurement reads the font file, why justified lines are placed word by word.
Adding an example figure¶
- Drop a script in one of the family directories.
- Give it
OUTPUT = "name.svg"andbuild() -> Doc. - End it with
if __name__ == "__main__": render(build(), OUTPUT).
The e2e suite finds it by directory scan and will start asserting that it builds, that nothing lands outside the canvas, and — for posters — that it fits the page. Nothing to register.
Parked: automatic layout¶
svg-plus places nothing for you. You put the boxes in rows and stacks; the library routes an arrow between two of them and no further. That is a deliberate boundary, and it is worth writing down what would have to change for it to move.
What is parked, as one piece of work, because they are the same problem:
- Automatic node placement — give it a set of nodes and edges and have it decide the arrangement, the way dagre, ELK, TALA and Graphviz do.
- Obstacle-aware routing — steering an edge around the boxes between its endpoints by itself.
connect(..., via="left")is the manual form and covers the common case, which is a back-edge across several tiers; what is parked is working the side out automatically.
Why not now. In a figure worth drawing, the arrangement is the argument: these three sit in a layer, that one spans the others, this tier is above that one. An engine will move them. Every figure in examples/ is placed deliberately, and none of them is a graph whose shape we do not control. Building a layout engine also means competing with four mature ones on their own ground, and losing the property that makes this library worth having.
What to do instead. Diagram-as-code tools all emit SVG, and Image embeds SVG:
You get their layout inside your page format, theme and typography. The honest caveat is that the embedded diagram keeps its fonts and colours, so it will look like a D2 diagram sitting in your figure unless you match the theme by hand.
What would change our mind. A real figure that cannot be placed by hand — one whose node set comes from data and varies in size, so no arrangement can be written down in advance. At that point the answer is probably still to shell out to Graphviz for coordinates and draw the result with svg-plus blocks, rather than to write a layout engine.
Things that are known and deliberate¶
| No bold-italic face | A theme carries three faces. Pass a fourth with Text(face=…). |
| No automatic hyphenation | It is a hook. Bundling a dictionary per language is not svg-plus's job. |
Stack and Row reject key= |
They take grow and width; use .named("x") instead. An inconsistency worth fixing. |
| Text can overflow | An unwrapped line, or a single word wider than its column, runs on. Loud beats silent. |
| Connectors ignore obstacles | A route uses its two endpoints only, unless you name a side with via=. See parked above. |
| A short hop cannot carry a label | The gutter between two adjacent boxes is narrower than a word. |