Skip to main content

mocodraw

English · Français

Draw entity-relationship diagrams from a text description.

CLIENT: Réf. client, Nom, Prénom, Adresse
PASSER, 0N CLIENT, 11 COMMANDE
COMMANDE: Num commande, Date, Montant
import mocodraw

print(mocodraw.preview(source))            # see it in the terminal
svg = mocodraw.render(source)              # get the SVG
mocodraw.render_to_file(source, "shop.svg", png=True)

The input language is the one Mocodo uses. This package is an independent implementation of that language and of the drawing it produces: the layout and the rendered output are validated against Mocodo itself, element by element.

Nothing beyond the standard library is required to parse, lay out and draw. PNG output is the single optional extra.

See it work first

python examples/demo_en.py             # a guided tour, English
python examples/demo_fr.py             # the same tour, French
python examples/gallery.py             # every example model at once

The tour walks through parsing, the ASCII preview, how the layout follows the source, how a cycle forces a second row, styles, the whole input language, and the errors the library produces. It prints to the terminal and writes examples/out/demo_en.html (or demo_fr.html) with the same walkthrough and the real SVG inlined, so the diagrams can be looked at. gallery.py writes examples/out/gallery.html the same way.

Install

pip install mocodraw         # from PyPI
pip install mocodraw[png]    # ... with PNG output (also needs the cairo library)

Until it is published, install from a checkout with pip install ..

Then, either as a library:

import mocodraw

or from the command line:

mocodraw model.mcd                     # writes model.svg
mocodraw model.mcd --preview           # prints the diagram to the terminal

Raster output

--png — or write(..., png=True) — also writes a PNG beside the SVG, and --png-scale 3 makes it large enough for a printed handout or a projector. This is the package's one optional extra: it needs cairosvg and the cairo library, and nothing else here needs either.

pip install 'mocodraw[png]'    # the cairosvg part
brew install cairo             # macOS;  apt install libcairo2 on Debian/Ubuntu

A Homebrew or MacPorts cairo is found without any environment variable, which is otherwise the usual snag on macOS. If cairo cannot be installed at all, the SVG is plain and self-contained, so any converter will do:

rsvg-convert -w 1600 model.svg -o model.png     # librsvg
resvg --width 1600 model.svg model.png          # resvg
inkscape --export-width=1600 model.svg          # Inkscape

Single file

dist/mocodraw_standalone.py is the whole package concatenated into one importable, runnable file, for places where nothing can be installed — an online Python service, a locked-down machine, a code snippet:

python dist/mocodraw_standalone.py     # prints a diagram

dist/mocodraw_selfcheck.py is the same bundle followed by a self-test of the parser, the layout, the renderer, the object layer, every style preset and every error path. It is what to paste into an online playground to check the library works in a bare runtime.

Both are generated; regenerate with:

python tools/bundle.py --mode demo      --out dist/mocodraw_standalone.py
python tools/bundle.py --mode selfcheck --out dist/mocodraw_selfcheck.py

Verified on online-python.com (stock CPython, no packages installed); the screenshot is in the repository at docs/online-python-selfcheck.png.

The input language

Each line is one of:

Line Meaning
NAME: attr, attr an entity
NAME, 0N A, 11 B: attr an association, with an optional attribute
: :: an empty cell, to align rows
% text a comment
/XT\ P <= C1, C2 an inheritance
(CIF) A, B: 50, 20 a constraint
(blank line) starts a new row of the grid

The layout is decided entirely by which line comes where: the order of lines is the order of boxes, a blank line starts a new row, and indentation puts boxes on successive pages. There is no coordinate syntax, which is what makes a model reviewable as a diff.

Attributes

Written Means
name a plain attribute
Réf. client [VARCHAR(50)] with a datatype (kept, not drawn in the MCD)
#customerId > CUSTOMER > customerId a foreign key: drawn as #customerId, plus a link to the referenced attribute
_name first not the identifier (the first attribute is one by default)
0_name adds a component to the main identifier
1_name, 2_other alternative identifier groups, numbered in a gutter column

Legs

Written Means
0N ENTITY cardinality 0,N
0N< ENTITY with an arrow head
-11 ENTITY cardinality hidden
_11 ENTITY a strengthening leg: makes the entity weak (dashed underline)
/11 ENTITY a cluster peg
0N [a note] ENTITY a note shown on hover

Library

import mocodraw

diagram = mocodraw.Diagram.from_source(source, colors="ocean", shapes="arial", scale=1.5)

diagram.model                     # entities, associations, inheritances, constraints
diagram.geometry                  # canvas size, box centres, per-leg tweaks
diagram.geometry_dict()           # the same, as plain JSON-serialisable data
diagram.svg()                     # the SVG document
diagram.preview()                 # ASCII art
diagram.overlaps()                # legs drawn on top of a box, if any
diagram.write("out.svg", png=True)

Convenience wrappers for the single-call cases:

mocodraw.render(source, **options)             # -> str
mocodraw.render_to_file(source, "out.svg")     # -> [Path]
mocodraw.preview(source)                       # -> str
mocodraw.parse(source)                         # -> Model

Options

Option Default Meaning
colors "ocean" a palette name, a dict, or a path to a palette JSON file
shapes "arial" a shape preset: arial, copperplate, georgia, mondrian, sans, serif, times, trebuchet, verdana, xinnian
scale 1.0 multiplies every geometry constant and font size
adjust_width 1.0 fudges text measurement only
id_gutter "auto" "on", "off"; "auto" shows the identifier gutter only when an entity has alternative identifiers
df_label "DF" the name that marks a dependency box
card_format "{min_card},{max_card}" how cardinalities are written
strengthen_card "_1,1_" the cardinality drawn on a strengthening leg
strong_id_label / weak_id_label "ID" / "id" the gutter labels for strong and weak identifiers
flex 0.75 how eagerly a leg is curved when its label would be hidden; 0 disables it
notes True emit the hover-note machinery

Any further keyword is written straight into the style, e.g. margin=30, entity_color="#ffffff", box_stroke_depth=2.

mocodraw.available_palettes() and mocodraw.available_shapes() list what is built in.

Build a model in Python

Writing .mcd by hand suits a small model; a program that generates diagrams wants typed objects. Schema is that layer, and it writes the Mocodo source for you.

from mocodraw import Association, Entity, Schema

schema = Schema(
    Entity("CLIENT", "customerId", ["lastName", "firstName", "city"]),
    Association("PLACE", [
        ("CLIENT", "right", "0,N"),          # (entity, direction, cardinality)
        ("ORDER_RECORD", "left", "1,1"),
    ]),
    Entity("ORDER_RECORD", "orderId", ["orderDate", "status"]),
)

print(schema.to_mcd())     # the Mocodo source
schema.render()            # straight to SVG
schema.directions()        # the side each leg really leaves on
  • Entity(name, primary_key, attributes) — the key comes first and is underlined. A composite key is a list, and every component after the first is written 0_.
  • Entity(..., weak=True) — optional. It finds the 1,1 leg pointing at the entity, which underlines that cardinality and switches the key from a solid rule to a dashed one. If no leg qualifies, or several do, nothing is marked and Schema.warnings says why.
  • Association(name, legs, attributes) — a leg is (entity, direction, cardinality), a Leg object, or a shorter tuple. identifying=True underlines the cardinality, and must be 1,1, because that is the only cardinality Mocodo will underline.
  • direction is right, left, top or bottom: the side of the entity the association sits on.

The direction is the interesting part. Mocodo has no coordinate syntax — a box's position, and therefore the side a leg leaves on, follows from the order of the lines and the blank lines between them. So to_mcd solves for a grid whose layout really does put each box where the legs ask, emits that grid explicitly, and then checks the drawing against the request.

Nothing blocks you, and nothing is silently dropped. The builder always produces a diagram, and anything it could not honour is written down: layout compromises in schema.conflicts, model remarks in schema.warnings, both echoed as % comments in the emitted file. Call to_mcd(strict=True) when you would rather have an exception than a best effort.

to_mcd(layout=...) decides the arrangement yourself: "single" for one row in declaration order, or a list of rows such as [["A", None, "L", "B"]] where None is an empty cell. Empty cells are emitted as :, so the layout is exactly the one asked for.

Writing your own model

Three entry points, and the working loop is the same for all of them.

You have Write Then
a model you want to edit by hand Mocodo text in a .mcd file mocodraw model.mcd --preview, --check, then render it
a model a program should generate Entity / Association / Schema objects schema.to_mcd() to get the text back, or schema.render() for the SVG
a model already written for Mocodo just point this library at it mocodraw.render(source)

The loop that keeps you out of trouble is preview, check, render:

mocodraw modele.mcd --preview     # see it in the terminal, no file written
mocodraw modele.mcd --check       # a leg through a box? rearrange, then retry
mocodraw modele.mcd               # write modele.svg

--check is the one worth making a habit. Legs are straight segments between box centres, so a box that happens to sit between two connected boxes will have a line running through it; the fix is almost always a blank line, which starts a new row. When you build with objects, the same two signals arrive as diagram.overlaps() and schema.conflicts.

Command line

mocodraw model.mcd                                  write model.svg
mocodraw model.mcd -o out.svg --colors bw --scale 1.5
mocodraw model.mcd --preview                        ASCII, no file written
mocodraw model.mcd --embed-preview                  both
mocodraw model.mcd --check                          report overlapping legs, exit 1 if any
mocodraw model.mcd --geometry --preview             dump the computed geometry as JSON
mocodraw model.mcd --png --png-scale 2
mocodraw -                                          read the source from stdin

--check earns its keep: legs are routed straight between box centres, so a box that happens to sit between two connected boxes will have a line running through it. When it reports something, insert a blank line to break the model into more rows.

Fidelity

The layout algorithm and the rendered output are validated against Mocodo 4.3.3 itself, not against hand-written expectations. Two tools do the comparison, and tests/test_against_mocodo.py runs both as part of the test suite (skipped when Mocodo is not installed):

python tools/compare_with_mocodo.py     # canvas size and every box centre
python tools/compare_svg.py -v          # every line, rect, circle, polygon, path, label

Current results on this machine:

  • 10 reference models — chains, multi-row models with cycles, reflexive associations, inheritance, alternative identifiers, foreign keys, dependency boxes, hidden boxes, empty cells — identical, canvas and every box centre;
  • every drawn primitive identical: not only the boxes and the lines, but the path data of every rounded outline, polygon and arrow head, and the colours and font names as well; checked for the arial, georgia, mondrian, times, verdana, xinnian and trebuchet presets, for all seven palettes tried, and for scales 0.5 through 2.4;
  • the character widths the layout depends on agree with Mocodo's font_metrics.json exactly, except for a handful of characters that the system fonts do not contain (recorded in mocodraw/_font_data.ABSENT), and for which each side falls back to its own default.

What the comparison covers, and what it once missed

This claim was once weaker than it sounded. The comparison read only attributes whose value is a single number, which silently skipped d and points — the geometry of every rounded outline, polygon and arrow head — and every colour. Two real bugs survived behind that: an association's body fill hanging below its box by the height of the title band, and inheritance arrow heads pointing at the association instead of at the entity. The first was spotted by eye, the second while widening the comparison.

The tool now parses and compares path data, coordinate lists, colours and font names, and tests/test_artifacts.py checks that it really notices a one-pixel change in a path, a one-step change in a colour, and a moved polygon vertex. So the honest form of the claim is: the output is identical as far as the comparison reaches, the comparison is itself tested, and its blind spots have been found and closed once.

Known deviations

Three shape presets cannot match on this machine, and the tests mark them as expected failures rather than hiding them:

Preset Why
sans, serif Mocodo's bundled font table has no Liberation Sans or Liberation Serif, so it silently substitutes Courier New. We measure the real fonts.
copperplate Mocodo resolves Copperplate to the Light face and Futura to the condensed one; we measure the regular faces.

All three come down to which physical font file a family name resolves to, not to the layout. On a machine whose font table includes those families, Mocodo would agree with us. There are three more deliberate differences, all cosmetic:

  • no interactive hover-outside-the-canvas note card, only the in-canvas one;
  • no separate _static.svg output;
  • --shapes defaults to arial rather than Mocodo's copperplate, because Arial is the preset whose metrics are exactly reproducible everywhere.

Cluster hulls are drawn only for three-legged clusters, and are suppressed when the diagram contains a CIF constraint. Hulls of four or more legs need a convex-hull routine that no classroom diagram has ever required.

How it works

There are two ways in: Mocodo source text, or the objects of mocodraw.schema, which emit source text and then take the same path.

source text
    │  parser.py      lines -> clauses (entities, associations, legs, attributes)
    ▼
clauses
    │  document.py    linking, identifier rules, grid rows, pages
    ▼
Model
    │  style.py       palette + shape preset + overrides
    │  metrics.py     character advance widths, from _font_data
    │  layout.py      measure, widen columns, stack rows, compress
    ▼
Model + Style + Geometry
    │  svg.py         drawing operations -> SVG
    │  preview.py     drawing operations -> ASCII
    ▼
output

The layout is a fixed grid, which is what makes it deterministic and explainable: a box's width comes from its longest line of text, all boxes in a column take the width of the widest, rows are stacked with room for the cardinality labels between them, and then over-wide columns and over-tall rows are pulled back. The same source therefore produces the same coordinates on every machine, and tests/test_layout.py asserts that.

Boxes describe themselves as a list of backend-agnostic drawing operations (("rect", {...})), and the SVG and ASCII renderers both consume that list. Adding a third backend means writing one more serialiser, not touching the layout.

Text measurement is the one place where the output is not machine-independent, because it depends on the font tables in mocodraw/_font_data.py. Those are generated from the fonts installed on the machine that ran tools/gen_font_metrics.py; the generated file is committed, so ordinary users never need the generator.

Development

pip install -e '.[dev]'          # pytest, build, twine, fontTools
pip install -e '.[reference]'    # mocodo, for the fidelity comparisons below
pytest -q                       # the reference comparisons skip without mocodo
python tools/gen_font_metrics.py --verify   # regenerate and diff the font tables
python tools/gen_palettes.py                # regenerate the colour schemes
python tools/bundle.py --mode demo          # regenerate the single-file bundles
python examples/demo_en.py                  # regenerate the demo pages

ruff configuration and the test layout are in pyproject.toml. Releasing is documented in docs/PUBLISHING.md. The demo's commentary lives in one table with an English and a French column, so the two cannot drift apart; the table is in examples/demo.py.

Licence

GPL-3.0-or-later.

The code here is an independent implementation, but the input language, the layout algorithm and the shape presets are Mocodo's, and Mocodo is GPL-3.0. Matching its behaviour as closely as this package does makes the GPL the honest choice. Mocodo itself is by Aristide Grange and is worth using directly if you want its converter targets (relational schema, SQL DDL, Chen and crow's-foot notations, layout optimiser).

The font tables in _font_data.py are measurements of the fonts installed on the machine that generated them; advance widths are functional facts about a font, not creative content.

Metadata

Release files for mocodraw 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mocodraw 0.1.0
File Size Uploaded
mocodraw-0.1.0.tar.gz 471.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mocodraw 0.1.0
File Interpreter ABI Platform
mocodraw-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 557.4 kB

Release files / mocodraw-0.1.0.tar.gz

Download URL mocodraw-0.1.0.tar.gz
Size 471.3 kB
Tags Source
SHA-256 checksum
How to use checksums
113ad2826f850e4c73cf20cf2469d15b171e8d4f8157bb08fda37491da655133
BLAKE2b-256 checksum
How to use checksums
b4ef755dd12d8229f090116fff929e63ec04c4f3e62ac9bf22d8382c587ac3e4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Release files / mocodraw-0.1.0-py3-none-any.whl

Download URL mocodraw-0.1.0-py3-none-any.whl
Size 86.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d41c0921970d006887f7b87f5147707d90f833b19df8eb0e1dfd44866081d2cf
BLAKE2b-256 checksum
How to use checksums
606752894462958fe23289ed993a4bbeb0b311c032ddb8fa5341280fed930dcd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page