Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,19 @@ jobs:
python-version: ${{ matrix.python-version }}
- run: uv sync --locked
- run: uv run pytest

examples:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v7
- run: uv sync --locked --extra bin
- run: |
project="$PWD"
output="$(mktemp -d)"
cd "$output"
for example in "$project"/examples/*.py; do
echo "::group::$example"
"$project/.venv/bin/python" "$example"
echo "::endgroup::"
done
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,7 @@ _cache/
**/*/*test*.jpeg
**/*/*test*.jpg
**/*/*test*.png

# example output
/examples/*.svg
/examples/*.d2
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ This can be rendered using `d2 graph.d2 graph.svg && open graph.svg` or [https:/

![example graph](/docs/images/d2.svg)

Pick a shape type with `shape=d2.ShapeType.<name>` and add a tooltip with `tooltip=`:

```python
db = d2.Shape(
name="db",
shape=d2.ShapeType.cylinder,
tooltip="Primary database",
)
```

A tooltip is shown when you hover the shape in the rendered SVG. Connections take `tooltip=` too.

## Rendering

`d2.render` runs the d2 command and returns the image as bytes. It uses the binary from `d2-bin` when that is installed, and otherwise looks for `d2` on your PATH.
Expand All @@ -77,7 +89,7 @@ with open("graph.svg", "wb") as f:
print(d2.render(diagram, "txt").decode())
```

The format can be `"svg"` (the default), `"png"`, `"pdf"` or `"txt"`, which is ASCII art. You can also pass `layout="dagre" | "elk" | "tala"` and `theme=<id>`. A source string works in place of a diagram. If d2 fails, `d2.D2Error` is raised with the message from d2. PNG and PDF make d2 download a headless browser the first time you use them.
The format can be `"svg"` (the default), `"png"`, `"pdf"` or `"txt"`, which is ASCII art. You can also pass `layout="dagre" | "elk" | "tala"` and `theme=<id>`. For a diagram with layers, scenarios or steps, `target=` picks the board: the root by default, or a path such as `"layers.detail"`. A source string works in place of a diagram. If d2 fails, `d2.D2Error` is raised with the message from d2. PNG and PDF make d2 download a headless browser the first time you use them.

See the [tests](/tests/test_d2) for more detailed usage examples.

Expand Down Expand Up @@ -116,7 +128,7 @@ import d2
diagram = d2.Diagram(
comments=["Generated by d2-python"],
direction="right",
vars=d2.Config(layout_engine="elk", theme_id=4, sketch=True),
vars=d2.Config(layout_engine="tala", theme_id=4, sketch=True),
classes={"hot": d2.Style(fill="orange", bold=True)},
shapes=[
d2.Shape(name="api", tooltip="Public API", width=200, classes=["hot"]),
Expand Down Expand Up @@ -152,11 +164,7 @@ Use `layers`, `scenarios` and `steps` for other boards. Use `comments` (or `add_

## Examples

See [`examples/`](examples/):

```sh
uv run python examples/simple_sql_schema.py
```
Runnable examples for every feature are in [`examples/`](examples/README.md).

## Development

Expand Down
26 changes: 26 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Examples

Each script builds one diagram and writes it next to where you run it. Install the d2 binary with the `bin` extra, then run any example:

```bash
uv sync --extra bin
uv run python examples/shapes.py
```

| Example | What it shows |
| --- | --- |
| [basic.py](basic.py) | Shapes, labels and a connection |
| [shapes.py](shapes.py) | Shape types such as cylinder, queue and person, and icons |
| [containers.py](containers.py) | Shapes inside shapes, and connections to nested shapes with dotted paths |
| [styles.py](styles.py) | Fill, stroke, font, bold, opacity, 3d, shadow and border radius |
| [connections.py](connections.py) | Labels, directions, styles and arrowheads, including crow's foot |
| [tooltips_and_links.py](tooltips_and_links.py) | Tooltips on shapes and connections, and shape links |
| [text_blocks.py](text_blocks.py) | Markdown and code text inside a shape |
| [grids.py](grids.py) | Grid layout with columns and gap |
| [classes.py](classes.py) | Reusable style classes |
| [sql_tables.py](sql_tables.py) | SQL tables with constraints and a foreign key |
| [uml_classes.py](uml_classes.py) | UML class shapes with fields, methods and inheritance |
| [boards.py](boards.py) | Layers, scenarios and steps, rendering the root board and one layer |
| [config.py](config.py) | Layout engine, theme, direction and sketch mode through `vars` |
| [ascii.py](ascii.py) | ASCII output printed with `d2.render(diagram, "txt")` |

13 changes: 13 additions & 0 deletions examples/ascii.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import d2


def main() -> None:
diagram = d2.Diagram(
shapes=[d2.Shape(name="a"), d2.Shape(name="b")],
connections=[d2.Connection("a", "b", "hello")],
)
print(d2.render(diagram, "txt").decode())


if __name__ == "__main__":
main()
18 changes: 18 additions & 0 deletions examples/basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pathlib import Path

import d2


def main() -> None:
diagram = d2.Diagram(
shapes=[
d2.Shape(name="client", label="Web client"),
d2.Shape(name="server", label="API server"),
],
connections=[d2.Connection("client", "server", "request")],
)
Path("basic.svg").write_bytes(d2.render(diagram))


if __name__ == "__main__":
main()
32 changes: 32 additions & 0 deletions examples/boards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from pathlib import Path

import d2


def main() -> None:
detail = d2.Diagram(
shapes=[d2.Shape(name="api"), d2.Shape(name="db")],
connections=[d2.Connection("api", "db")],
)
diagram = d2.Diagram(
shapes=[d2.Shape(name="system")],
layers=[d2.Layer(name="detail", diagram=detail)],
scenarios=[
d2.Layer(
name="db_down",
diagram=d2.Diagram(
shapes=[d2.Shape(name="db", style=d2.Style(fill="red"))]
),
)
],
steps=[
d2.Layer(name="one", diagram=d2.Diagram(shapes=[d2.Shape(name="start")])),
d2.Layer(name="two", diagram=d2.Diagram(shapes=[d2.Shape(name="finish")])),
],
)
Path("boards.svg").write_bytes(d2.render(diagram))
Path("boards-detail.svg").write_bytes(d2.render(diagram, target="layers.detail"))


if __name__ == "__main__":
main()
23 changes: 23 additions & 0 deletions examples/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pathlib import Path

import d2


def main() -> None:
diagram = d2.Diagram(
classes={
"service": d2.Style(fill="lightblue", stroke="blue", border_radius="8"),
"store": d2.Style(fill="lightgreen", stroke="green"),
},
shapes=[
d2.Shape(name="api", classes=["service"]),
d2.Shape(name="worker", classes=["service"]),
d2.Shape(name="db", shape=d2.ShapeType.cylinder, classes=["store"]),
],
connections=[d2.Connection("api", "db"), d2.Connection("worker", "db")],
)
Path("classes.svg").write_bytes(d2.render(diagram))


if __name__ == "__main__":
main()
17 changes: 17 additions & 0 deletions examples/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pathlib import Path

import d2


def main() -> None:
diagram = d2.Diagram(
direction="right",
vars=d2.Config(layout_engine="tala", theme_id=4, sketch=True, pad=40),
shapes=[d2.Shape(name="a"), d2.Shape(name="b"), d2.Shape(name="c")],
connections=[d2.Connection("a", "b"), d2.Connection("b", "c")],
)
Path("config.svg").write_bytes(d2.render(diagram))


if __name__ == "__main__":
main()
34 changes: 34 additions & 0 deletions examples/connections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from pathlib import Path

import d2


def main() -> None:
diagram = d2.Diagram(
direction="right",
shapes=[d2.Shape(name=name) for name in ("a", "b", "c", "d", "e")],
connections=[
d2.Connection("a", "b", "sends"),
d2.Connection("b", "a", direction=d2.Direction.FROM),
d2.Connection("a", "c", "both ways", direction=d2.Direction.BOTH),
d2.Connection("c", "d", direction=d2.Direction.NONE),
d2.Connection(
"d",
"e",
"styled",
style=d2.Style(stroke="red", stroke_dash=3, animated=True),
target_arrowhead=d2.Arrowhead(d2.ArrowheadShape.DIAMOND, filled=True),
),
d2.Connection(
"e",
"a",
source_arrowhead=d2.Arrowhead(d2.ArrowheadShape.CF_ONE, label="1"),
target_arrowhead=d2.Arrowhead(d2.ArrowheadShape.CF_MANY, label="n"),
),
],
)
Path("connections.svg").write_bytes(d2.render(diagram))


if __name__ == "__main__":
main()
29 changes: 29 additions & 0 deletions examples/containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from pathlib import Path

import d2


def main() -> None:
diagram = d2.Diagram(
shapes=[
d2.Shape(
name="backend",
shapes=[
d2.Shape(name="api"),
d2.Shape(name="worker"),
d2.Shape(name="cache", shapes=[d2.Shape(name="redis")]),
],
connections=[d2.Connection("api", "worker", "enqueue")],
),
d2.Shape(name="frontend"),
],
connections=[
d2.Connection("frontend", "backend.api", "HTTP"),
d2.Connection("backend.worker", "backend.cache.redis", "reads"),
],
)
Path("containers.svg").write_bytes(d2.render(diagram))


if __name__ == "__main__":
main()
21 changes: 21 additions & 0 deletions examples/grids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pathlib import Path

import d2


def main() -> None:
diagram = d2.Diagram(
shapes=[
d2.Shape(
name="board",
grid_columns=3,
grid_gap=10,
shapes=[d2.Shape(name=f"cell{i}") for i in range(1, 7)],
),
],
)
Path("grids.svg").write_bytes(d2.render(diagram))


if __name__ == "__main__":
main()
24 changes: 24 additions & 0 deletions examples/shapes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pathlib import Path

import d2

ICON = "https://icons.terrastruct.com/essentials%2F112-server.svg"


def main() -> None:
diagram = d2.Diagram(
direction="right",
shapes=[
d2.Shape(name="db", label="Database", shape=d2.ShapeType.cylinder),
d2.Shape(name="jobs", shape=d2.ShapeType.queue),
d2.Shape(name="user", shape=d2.ShapeType.person),
d2.Shape(name="decision", shape=d2.ShapeType.diamond),
d2.Shape(name="internet", shape=d2.ShapeType.cloud),
d2.Shape(name="server", icon=ICON),
],
)
Path("shapes.svg").write_bytes(d2.render(diagram))


if __name__ == "__main__":
main()
22 changes: 0 additions & 22 deletions examples/simple.py

This file was deleted.

43 changes: 0 additions & 43 deletions examples/simple_sql_schema.py

This file was deleted.

Loading
Loading