Skip to main content
easydot

High-quality Graphviz plots from Python, with browser, WASM, and native backends.

pip install easydot License: BSD-3-Clause marimo Open in molab

pip install easydot
import easydot

easydot.render("digraph { A -> B -> C }")

Themes

Theme supplies Graphviz defaults for a graph, its nodes, and its edges. The theme statements are inserted immediately inside the root graph, so normal Graphviz source-order rules apply: later DOT default statements affect elements created after them, and explicit per-element attributes win for those elements. Themes do not retroactively restyle earlier DOT statements.

from easydot import Theme

theme = Theme(
    graph={"rankdir": "LR", "bgcolor": "transparent"},
    node={"shape": "box", "style": "rounded,filled", "fillcolor": "#eef2ff"},
    edge={"color": "#64748b", "arrowsize": 0.7},
)

dot = "digraph { A -> B -> C }"
easydot.plot(dot, theme=theme)

# Inspect or pass the prepared DOT to another Graphviz tool.
styled_dot = theme.apply(dot)

Themes can be extended without modifying the original:

publication = theme.extend(node={"fontname": "Helvetica", "fontsize": 10})

Themes can also expose generic named styles for callers that build individual nodes or edges programmatically. A named lookup returns only that style (plus any overrides); the global node and edge mappings remain fallback defaults applied by theme=.

biology = theme.extend(
    nodes={"gene": {"shape": "box", "fillcolor": "#dbeafe"}},
    edges={"activation": {"color": "#15803d", "arrowhead": "normal"}},
)

gene_attrs = biology.node_attrs("gene", label="TP53")
edge_attrs = biology.edge_attrs("activation")

Named styles can be expanded explicitly by callers without requiring pydot. For example, with optional pydot installed, a caller can use a named style while building a graph:

# Optional dependency: pip install pydot
import pydot

graph = pydot.Dot(graph_type="digraph")
graph.add_node(pydot.Node("TP53", **biology.node_attrs("gene")))
easydot.plot(graph, theme=biology)

Preset vocabularies

The optional easydot.themes module provides small, ordinary Theme instances for common research diagrams:

  • biology.nodes: gene, rna, protein, transcription_factor
  • biology.edges: interaction, activation, inhibition
  • signaling adds receptor, complex, small_molecule, phenotype, and phosphorylation
  • gene_regulation adds promoter and transcription

The specialized presets inherit the global defaults and all styles from biology:

from easydot.themes import biology, gene_regulation, signaling

graph.add_node(pydot.Node("EGFR", **signaling.node_attrs("receptor")))
graph.add_edge(pydot.Edge("TP53", "EGFR", **signaling.edge_attrs("activation")))
easydot.plot(graph, theme=signaling)

Presets are data, so domain-specific builders can extend them directly:

metabolism = biology.extend(
    nodes={"metabolite": {"shape": "circle", "fillcolor": "#E0F2FE"}},
    edges={"conversion": {"arrowhead": "normal"}},
)

Named style attributes are materialized when a concrete node or edge is built; theme= supplies only the graph/node/edge defaults and does not infer roles from DOT. If a preset or renderer theme changes, rebuild programmatically constructed elements to update their named-style appearance. Explicit DOT attributes still win according to normal Graphviz source-order rules.

Example

easydot example

💡 Why easydot

Graphviz is the best way to lay out DOT graphs, but the right runtime depends on where your code is running. Native dot is great when it is installed; browser rendering is better in notebooks and sandboxed frontends; server-side WASM is useful when you want static SVGs without system binaries.

easydot gives all three paths a small Python API.

  • One entry point. easydot.render(...) returns a rich notebook display object; easydot.to_string(...) returns raw HTML or SVG.
  • Three backends. browser uses JS/WASM in the frontend, wasm uses wasi-graphviz in Python, and native shells to installed Graphviz executables.
  • Pip-installable default. The browser backend has no Python dependencies and does not require brew, conda, apt-get, or Dockerfile changes.
  • Tiny notebook outputs. The WASM bundle is vendored and served once over loopback instead of inlined into every cell.
  • Offline-capable. Browser assets ship in the package; server-side backends do not need browser network access.

🔤 Why DOT

DOT is a small text format for graph diagrams. Many Python libraries and build tools can generate it.

  • Common output format. NetworkX, pydot, pygraphviz, scikit-learn decision trees, PyTorch and TensorFlow model viz, Dask task graphs, Airflow DAGs, Terraform, Bazel, Ninja, gprof2dot, and other tools can emit DOT.
  • LLM-friendly. Models can usually generate DOT for architecture diagrams, state machines, and dependency graphs.
  • Plain text. Diffs cleanly, templates easily, pipes nicely.
  • Graphviz features. Five layout engines (dot, neato, fdp, circo, twopi), clusters, HTML-like labels, and styling.

🚀 Usage

Quick start

render() is the main interface. It returns a Graph object that displays in Jupyter, marimo, and other rich-output environments. Use backend="auto" (the default) to select the first working backend, or pick one explicitly.

import easydot

# Auto-select the best available backend (native → wasm → browser)
easydot.render("digraph { A -> B -> C }")

# Explicit backends
easydot.render("digraph { A -> B -> C }", backend="browser")   # browser JS/WASM
easydot.render("digraph { A -> B -> C }", backend="wasm")      # server-side WASM
easydot.render("digraph { A -> B -> C }", backend="native")    # native Graphviz

# Fit and scale work on all backends
easydot.render("digraph { A -> B -> C }", fit="horizontal")
easydot.render("digraph { A -> B -> C }", fit="both", scale=1.5)

# Raw output
easydot.svg("digraph { A -> B -> C }")                       # SVG string (wasm/native)
easydot.html("digraph { A -> B -> C }", fit="horizontal")    # display-ready HTML
easydot.native("digraph { A -> B -> C }", format="png")      # PNG bytes
easydot.plot("digraph { A -> B -> C }")                       # static SVG display

SVG glyphs

SvgGlyph lets you draw a node with your own self-contained SVG while Graphviz still lays out the graph and clips edges to a built-in ellipse or rectangular box. Give the proxy node an explicit DOT id; easydot overlays the glyph on that proxy in the final SVG.

from easydot import SvgGlyph

dot = '''digraph {
  cell [id="cell-glyph", shape=ellipse, fixedsize=true,
        width=1.7, height=1.2, label="", color=transparent]
  cell -> next
}'''
glyphs = {"cell-glyph": SvgGlyph.from_file("cell.svg")}

easydot.render(dot, glyphs=glyphs)  # works with browser, WASM, and native SVG

The glyph is stretched to the proxy's bounds, so design its SVG canvas to match the proxy aspect ratio. Edges connect to the ellipse or box boundary, not to an arbitrary outline inside the artwork. Custom glyphs currently require SVG output; plot(..., format="png", glyphs=...) is unsupported.

Backend guide

Backend Runtime Fit/scale Best for
browser frontend JS/WASM ✓ notebooks, marimo, JupyterLite, Pyodide
wasm Python WASI runtime ✓ saved notebooks, GitHub, CI without Graphviz
native Graphviz executable ✓ local/conda/server environments with Graphviz

Check what works in the current runtime:

caps = easydot.capabilities()
caps["browser"].available   # True if local or CDN browser assets are reachable
caps["wasm"].available      # True if wasi-graphviz can render a probe graph
caps["native"].available    # True if native dot can render a probe graph

# Format-aware synchronous backends for plot()
easydot.static_capabilities(format="png")

backend="auto" uses these probes and chooses native, then wasm, then browser with CDN assets, then browser with local assets. Probe results are cached in-process; pass refresh_capabilities=True to render(..., backend="auto") or refresh=True to capabilities() if the runtime changes after startup.

Server-side WASM

For static SVG output that works in saved notebooks and GitHub without a live browser runtime:

pip install easydot[wasm]
import easydot

# Raw SVG string
svg = easydot.svg("digraph { A -> B -> C }", backend="wasm")

# Rich display object for notebooks — fit and scale work the same as browser
easydot.render("digraph { A -> B -> C }", backend="wasm", fit="horizontal")

# Display-ready HTML with fit/scale
html = easydot.html("digraph { A -> B -> C }", backend="wasm", fit="both")

Static notebook plots

Use plot() when the notebook output must be produced synchronously and embedded as a self-contained image, including notebooks executed with Papermill. It prefers native Graphviz and falls back to the Python WASM backend; it never uses the asynchronous browser backend.

import easydot

# SVG is the default and remains sharp when displayed in a notebook.
easydot.plot("digraph { A -> B -> C }")

# Request a raster image when a PNG-capable backend is available, typically native Graphviz.
easydot.plot("digraph { A -> B -> C }", format="png")

PNG support depends on the selected Graphviz build. If neither native Graphviz nor the Python WASM build supports raster output, plot(format="png") fails clearly; use the default SVG output in that environment.

Native Graphviz

If Graphviz executables are installed and available on PATH, easydot can render through the native toolchain:

import easydot

svg = easydot.svg("digraph { A -> B -> C }", backend="native")
easydot.render("digraph { A -> B -> C }", backend="native", fit="horizontal")

# Non-SVG formats: native() returns bytes for binary formats
png_bytes = easydot.native("digraph { A -> B -> C }", format="png")
pdf_bytes = easydot.native("digraph { A -> B -> C }", format="pdf")

The native backend shells to the selected Graphviz engine, such as dot or neato, and fails if the executable is missing or Graphviz returns an error.

pydot

pip install easydot[pydot]
import easydot, pydot

graph = pydot.Dot("example", graph_type="digraph")
graph.add_edge(pydot.Edge("A", "B"))

easydot.render(graph)

NetworkX

import easydot, networkx as nx
from networkx.drawing.nx_pydot import to_pydot

G = nx.DiGraph([("A", "B"), ("B", "C"), ("A", "C")])
easydot.render(to_pydot(G))

CLI

# HTML output (default) — fit and scale work on all backends
echo 'digraph { A -> B }' | easydot                              # browser backend HTML
echo 'digraph { A -> B }' | easydot --backend auto              # best available backend
echo 'digraph { A -> B }' | easydot --backend wasm --fit horizontal   # WASM with fit
echo 'digraph { A -> B }' | easydot --backend native --scale 1.5      # native with scale

# Raw SVG (wasm or native only)
echo 'digraph { A -> B }' | easydot --format svg --backend wasm
echo 'digraph { A -> B }' | easydot --format svg --backend native

# Binary formats (native only)
echo 'digraph { A -> B }' | easydot --format png --backend native > graph.png
echo 'digraph { A -> B }' | easydot --format pdf --backend native > graph.pdf

easydot --urls                                                    # print asset server URLs

🔀 Source Modes

By default, easydot tries a pinned CDN URL first and falls back to the local server. Known hosted notebook environments skip the local server probe, because a Python-side 127.0.0.1 server is not browser-reachable there.

Mode Local CDN Best for
auto yes yes Most setups (default; CDN first, then local fallback)
local yes no Offline environments with no internet access
cdn no yes Remote hosts where 127.0.0.1 isn't browser-reachable
easydot.render("digraph { A -> B }", source="cdn")
Environment variables

Set a notebook-wide default without editing every call:

import os
os.environ["EASYDOT_SOURCE"] = "cdn"   # auto | local | cdn

Only applies when source="auto". Explicit source= arguments still win.

For hosted marimo environments that protect generated iframe file URLs, force a self-contained iframe:

os.environ["EASYDOT_IFRAME_MODE"] = "srcdoc"   # auto | managed | srcdoc | data

PyCharm notebooks are detected automatically and use a data: iframe because their output recycling can detach and reattach srcdoc iframes while scrolling. You can force that wrapper explicitly with EASYDOT_IFRAME_MODE="data".

The same modes are available per render call:

easydot.render("digraph { A -> B }", iframe_mode="data")

📓 marimo

Works out of the box. easydot detects marimo and uses its iframe display helper automatically, since marimo doesn't execute inline scripts from plain text/html outputs. All source modes work.

The managed iframe mode uses the installed notebook iframe helper when available; otherwise it falls back to srcdoc.

uv run marimo edit examples/demo.py                                   # edit the demo
uv run marimo run examples/demo.py --headless --port 2718 --no-token  # read-only preview

⏳ Large Graphs

Browser rendering is asynchronous relative to notebook cell execution: a cell can finish before the browser has loaded Graphviz WASM and produced the SVG. By default, easydot renders on the output iframe's main thread and shows an in-progress indicator while the graph is rendering. You can opt into Web Worker rendering for large graphs.

easydot.render(dot, worker=False)   # default: render on the output iframe's main thread
easydot.render(dot, worker="auto")  # try a worker, visibly fall back if unavailable
easydot.render(dot, worker=True)    # require a worker; no main-thread fallback

If worker rendering is unavailable and worker="auto" is used, easydot shows a warning before falling back to main-thread rendering. Large graphs may freeze that output iframe until Graphviz finishes in fallback mode.

🔌 Library Integration

For libraries that generate their own HTML, use the lower-level asset API:

from easydot import asset_urls

js_url = asset_urls()["js"]
const mod = await import(jsUrl);
const graphviz = await mod.Graphviz.load();
const svg = graphviz.layout("digraph { A -> B }", "svg", "dot");

Need server-side rendering to files? Use easydot.to_string(..., backend="wasm") or easydot.to_string(..., backend="native").

Runtime model

The asset server is intentionally narrow:

  • Binds only to 127.0.0.1
  • OS-assigned ephemeral port
  • Serves only known packaged files (no directory browsing)
  • Long-lived cache headers
  • Shuts down automatically when the Python process exits

📜 License

Component License
easydot Python code BSD-3-Clause
Vendored Graphviz WASM Apache-2.0, from @hpcc-js/wasm-graphviz. Pinned version in src/easydot/_version.py

Metadata

Release files for easydot 0.4.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 easydot 0.4.0
File Size Uploaded
easydot-0.4.0.tar.gz 681.4 kB Details

Built distribution (wheel)

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

Total release size: 1.4 MB

Release files / easydot-0.4.0.tar.gz

Download URL easydot-0.4.0.tar.gz
Size 681.4 kB
Tags Source
SHA-256 checksum
How to use checksums
94aa398bfeba76f0659b348cd97d9984a88fd1c705b0c927e8614129e5634227
BLAKE2b-256 checksum
How to use checksums
b76a5d0bcf7081f18669404bb76e6b60920e84c6c43a4d6fdd3e99f7f3784e45
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / easydot-0.4.0-py3-none-any.whl

Download URL easydot-0.4.0-py3-none-any.whl
Size 692.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f249c3f8e92f1632d3092020d289093322cf043202314b6e8118fbd9bd0ac285
BLAKE2b-256 checksum
How to use checksums
b24be90c7286ab5fcf56ffc0c0fa0bb67cc224d58d7307833ed0f6791e27dc1a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.16

2 release files

0.1.15

2 release files

0.1.14

2 release files

0.1.13

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

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