</>
Back to Blog
Visual 2026-05-22 9 min read

Graphviz, Properly Explained

Graphviz started at AT&T Bell Labs in 1991, written by Stephen North and Emden Gansner as a research tool for visualizing call graphs and flow charts. It was open-sourced in 2004, and almost everything that's tried to replace it in the thirty-five years since has either copied the DOT language wholesale or quietly used Graphviz's layout engine under the hood. The reason: laying out a directed graph so it doesn't look like spilled spaghetti is a hard, well-studied algorithmic problem, and the people who solved it were the people who built Graphviz.

GraphvizDOTLayoutMermaidD2Visualization

If you've ever generated a diagram with dot -Tpng input.dot > output.png, you've used Graphviz directly. If you've ever generated a Doxygen call graph, a pip dependency tree, a Linux kernel subsystem map, an Ansible inventory diagram, or a network topology in tcpdump --graphviz — you've used it indirectly. The DOT language has the rare property of being readable, writable by hand, and machine-generable, which is most of why it survived.

This post is for the person who's seen digraph G { ... } somewhere and would like to understand why and when to reach for it.

What it actually is

Graphviz is a layout engine. You give it a description of a graph — nodes, edges, optional attributes — and it computes positions for the nodes and routes for the edges so the result is readable. The output is one of several rendered formats: SVG, PNG, PDF, PostScript, an interactive xdot file, or just the computed coordinates as JSON.

The input language is DOT:

digraph deploy {
  rankdir=LR;
  client -> ingress -> api;
  api -> db;
  api -> cache;
  cache -> db [style=dashed, label="invalidate"];
}

That's a complete program. digraph opens a directed graph (use graph for undirected). rankdir=LR makes the layout horizontal instead of the default top-to-bottom. Edges are -> for directed, -- for undirected. Attributes go in square brackets.

The DOT language is a small DSL — roughly 500 keywords across all attributes, but you'll only ever use a few dozen. The dot man page and the online attribute reference at graphviz.org/doc/info/attrs.html are the canonical reference.

The seven layout engines

Graphviz ships seven separate algorithms, each suited to a different graph shape:

Engine Algorithm Best for
dot Hierarchical / Sugiyama DAGs, flowcharts, dependency graphs
neato Spring model (Kamada–Kawai) Small undirected graphs with weighted edges
fdp Force-directed (Fruchterman–Reingold) Small-to-medium undirected graphs, "blob" shapes
sfdp Multilevel force-directed Large undirected graphs (thousands of nodes)
circo Circular Cyclic structures, ring topologies
twopi Radial Tree-shaped data with one root
osage Cluster packing Heavily nested clustered graphs

dot is the default. If your graph is roughly hierarchical (and most code-generated graphs are: a dependency graph, a state machine, a flowchart), dot is what you want. The output has clean ranks, edges generally flow in one direction, and crossings are minimized.

For an undirected graph showing relationships among a few dozen nodes, neato or fdp look cleaner — dot will shoehorn it into ranks that don't reflect the underlying structure.

For very large graphs (thousands of nodes), sfdp is the only one that finishes in finite time. It uses a multilevel approach: collapses similar nodes, lays out the simplified graph, then expands. The tradeoff is the layout is approximate.

The CLI command names match the engine names: dot, neato, fdp, sfdp, circo, twopi, osage. The web tool here exposes all seven; pick the one that suits the graph shape, not the one most familiar.

How dot actually lays out a hierarchy

The Sugiyama-style algorithm dot uses runs in roughly four phases:

  1. Rank assignment. Assign each node to a horizontal rank (or vertical layer) so all edges go from lower to higher rank. Long edges between distant ranks get broken into segments via dummy nodes.
  2. Order within ranks. Permute nodes within each rank to minimize edge crossings between adjacent ranks. This is NP-hard in general; dot uses a heuristic and iterates.
  3. Coordinate assignment. Compute X coordinates within each rank and Y coordinates per rank, balancing aesthetic constraints (parents centered above children, similar nodes aligned).
  4. Edge routing. Draw splines between nodes that don't intersect node bounding boxes.

The output looks like an org chart for a reason — the algorithm essentially is an org-chart algorithm, generalized.

The main consequence for you as a user: the input order matters. If two nodes could equally appear in either order at a given rank, dot picks the order from the input. Reordering the lines of your DOT file can shift the layout. This is occasionally frustrating ("why does this look different after I sorted my edges?") and occasionally useful ("I can hint a good layout by ordering my nodes").

DOT syntax tour

The features you'll actually use:

Nodes with shape and label:

api [shape=box, style=filled, fillcolor=lightblue, label="API\nServer"];
db  [shape=cylinder, label="PostgreSQL"];

Shapes: box, ellipse (default), circle, diamond, cylinder, note, folder, tab, component, dozens more. \n in a label is a line break.

Edges with attributes:

api -> db [label="reads", style=dashed, color=red, arrowhead=empty];

Subgraphs (cluster) for visually grouping nodes:

subgraph cluster_backend {
  label = "Backend Services";
  style = filled;
  color = lightgrey;
  api;
  worker;
  scheduler;
}

The cluster_ prefix is required to make dot draw a bounding box. Without it, the subgraph is just a logical grouping with no visual effect.

HTML-like labels for tables and rich content:

node1 [shape=plaintext, label=<
  <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
    <TR><TD>Server</TD><TD>192.168.1.1</TD></TR>
    <TR><TD>Service</TD><TD>nginx</TD></TR>
  </TABLE>
>];

HTML labels parse a small subset of HTML — TABLE, TR, TD, FONT, B, I, U, SUB, SUP. They're how you embed multi-row, multi-column content. Not real HTML — don't try to embed images, scripts, or complex CSS.

Rank constraints to force layout:

{ rank=same; web; api; }   // web and api on the same horizontal rank
{ rank=min; users; }        // users at the top
{ rank=max; storage; }      // storage at the bottom

Useful when dot's default rank assignment doesn't match your mental model.

When dot produces an ugly graph

The two most common reasons:

  1. Wrong engine. A force-directed graph (e.g., a network topology with no clear hierarchy) jammed through dot looks bad because dot insists on ranks that don't exist in the data. Try neato or fdp.
  2. Too many crossings. If edge crossings are unavoidable (your graph isn't planar), dot will minimize them but can't eliminate them. Sometimes splitting into subgraphs or adding rank constraints helps.

Other adjustment knobs:

  • nodesep and ranksep — horizontal and vertical spacing between nodes / ranks. Default is 0.25 inches; bumping to 0.5 or 1 spreads things out.
  • splines — edge routing style. splines=ortho gives right-angle edges (good for circuit-like diagrams), splines=line gives straight edges (clutters more easily), default is curved Bezier splines.
  • overlap=false for neato/fdp to prevent node overlap, at the cost of more space.
  • compound=true plus lhead=cluster_x / ltail=cluster_y to draw edges between clusters, not between specific nodes within them.

Graphviz vs Mermaid vs D2

The competition for "diagram from text" looks like:

  • Graphviz / DOT (1991). Mature, stable, every Linux distro ships it, the most powerful layout engines, the most cryptic syntax for newcomers.
  • Mermaid (2014). JavaScript-based, embedded in Markdown editors (GitHub, GitLab, Notion, Obsidian), syntax inspired by DOT but more readable for simple cases. Layout is its weakest point — for non-trivial graphs, the result often looks worse than dot's. Mermaid's flowchart is roughly DOT with extra steps.
  • D2 (2022). Newer, designed by ex-Mermaid engineers, native layout via Graphviz/ELK plus its own engine, syntax meant to be friendlier than DOT. Production-ready as of 2024.
  • PlantUML (2009). Java-based, focused on UML (sequence, class, state diagrams). Uses Graphviz under the hood for some layouts.
  • Diagrams.net (formerly draw.io), Excalidraw, Lucidchart — interactive diagramming, not text-based.

The case for sticking with Graphviz/DOT in 2026: your diagram is a DAG, a dependency graph, a flowchart, or a hierarchy; you have the source-of-truth in code (a dependency graph generated from a build system, an architecture map you check into the repo); and you want stability. DOT files I wrote in 2010 still render correctly. Mermaid syntax has changed several times; D2 is too new to know.

The case for switching: your diagrams are sequence/class/state UML (PlantUML is purpose-built); your team works in GitHub/GitLab and benefits from inline rendering (Mermaid is supported there natively); your diagram is small enough that layout quality doesn't matter (any tool works).

The case nobody bothers making: a Graphviz layout on a complex DAG is better than what hand-drawn or auto-laid-out alternatives produce, and that quality difference compounds when the graph has more than a hundred nodes.

Generating DOT from code

The killer use case for Graphviz is that DOT is easy to generate. Half a page of Python or Go can walk a data structure, emit edges as a -> b;, and pipe to dot:

def emit_dot(graph):
    print("digraph G {")
    print("  rankdir=LR;")
    for node, attrs in graph.nodes.items():
        print(f'  "{node}" [label="{attrs["label"]}"];')
    for src, dst in graph.edges:
        print(f'  "{src}" -> "{dst}";')
    print("}")

This is how pip-graph, cargo deps, gradle dependencies --visualize, terraform graph, kubectl get ... -o dot (via kompose and friends), and a long list of other tools work. The diagram is generated from the source-of-truth on demand, never gets out of date, and doesn't require an interactive editor.

If you find yourself maintaining the same diagram across a SaaS tool and your codebase, the right move is to generate the diagram from the code (or vice versa) and pick one as the source of truth. DOT is well-suited for this because the syntax is plain text, version-controllable, and diff-friendly.

Common pitfalls

  • Picking the wrong engine. Hierarchical graph through dot, undirected through neato/fdp, large-scale through sfdp. The default isn't always right.
  • Forgetting cluster_ prefix on subgraphs. Without it, the box doesn't draw.
  • HTML labels that aren't actually HTML. The supported subset is small and Graphviz-specific.
  • Quoting issues. Node IDs with spaces, dashes, or punctuation must be quoted: "my node". Quoting an already-quoted node breaks parsing.
  • Layout instability. Reordering input lines can change layout. If you check rendered diagrams into version control, sort the input lines (or generate them deterministically) so diffs are meaningful.
  • Massive graphs through dot. Graphs over a few thousand nodes blow up runtime and produce unreadable output. Use sfdp and decide what to omit.
  • Edge labels overlapping. If a long edge label overlaps a node, try moving the label to a node attribute (xlabel for external label) or shortening.
  • Cluster + edge ambiguity. Edges between clusters use compound=true plus lhead/ltail on the edge. Without that, edges go to specific nodes and the cluster boundary looks wrong.
  • Embedding images. Possible via image= attribute, but file paths are resolved relative to where dot is run, which is usually wrong in CI. Inline base64 SVG is a hack that mostly works.
  • Trying to round-trip a DOT layout. dot is one-way: input → image. There's no way to manually nudge a node and have the change persist as DOT input.

When to reach for Graphviz vs not

Use Graphviz when:

  • The diagram is generated from source code, configuration, or a database.
  • The graph is a DAG, dependency graph, or hierarchy with more than a handful of nodes.
  • You want a text format that diffs cleanly in version control.
  • Layout quality matters more than authoring speed.

Use something else when:

  • The diagram is small (~10 nodes) and a quick Mermaid block is enough.
  • You're documenting in a Markdown ecosystem with native Mermaid support.
  • The diagram needs UML-specific shapes (sequence, class) — PlantUML.
  • You need interactive editing — diagrams.net or Excalidraw.

The closest thing to "the right answer" for technical architecture diagrams in 2026 is: keep the source in DOT, render with Graphviz, commit the rendered SVG alongside the source. Anyone with dot installed can re-render the diagram from your repo; reviewers see the SVG without needing the toolchain. The code is the source of truth; the image is the artifact.

That workflow is what Graphviz was designed for in 1991, and it has aged surprisingly well.

Render DOT diagrams in your browser

The Graphviz tool on this site renders DOT graphs locally using the WebAssembly build of the upstream engine, with all seven layout engines (dot, neato, fdp, sfdp, circo, twopi, osage) and SVG / PNG export. Useful for sketching architecture diagrams, dependency graphs, or flowcharts that won't be locked into a SaaS tool. Nothing leaves your browser.

Open the Graphviz tool

Related guides

Keep the session useful with adjacent reading instead of exiting after one article.

View all guides

Cookie Consent

We use cookies to enhance your experience and show relevant ads. You can customize your preferences.