Graphviz and the DOT Language: A Complete Guide
Graphviz turns a plain-text description of nodes and edges into an automatically laid-out graph. This guide covers the DOT language, the attributes that control appearance, and the layout engines that place everything.
Graphviz is the original diagram-as-code tool: you describe a graph as nodes and edges in a simple language called DOT, and Graphviz computes a clean layout automatically. It has quietly powered a huge amount of technical diagramming for decades, and it sits underneath other tools too - PlantUML delegates much of its layout to Graphviz. If you have ever seen a tidy dependency graph or automaton diagram in documentation, there is a good chance DOT and Graphviz produced it.
This guide is a practical tour of DOT and Graphviz: the graph and digraph syntax, the node and edge attributes that control appearance, the rankdir and layout engines that determine placement, and subgraphs and clusters for structure. DOT is a text format, so you can reference any graph here and rebuild it as an editable, styled diagram in Atlas Diagram Studio at /diagrams. The diagram-as-code guide at /guides/diagram-as-code-guide sets the broader context, and /guides/plantuml-vs-graphviz compares Graphviz with the UML-focused PlantUML.
The core DOT syntax
A DOT file declares one graph. For a directed graph - arrows with direction - you write digraph G { ... } and connect nodes with the arrow operator, so digraph { a -> b } draws a pointing from a to b. For an undirected graph, use graph G { ... } and connect nodes with a double hyphen instead, so graph { a -- b } draws a plain line. The keyword and the edge operator must match: arrows with digraph and dashes with graph, or Graphviz will reject the file.
Nodes are created implicitly the first time you name them in an edge, exactly as in PlantUML, so you rarely declare them separately unless you want to set attributes. You can chain edges - a -> b -> c draws two edges in a line - and add many at once. Statements end with a semicolon, which is optional but conventional. That is the entire structural core: a graph type, a set of nodes, and the edges between them, all in a few lines of readable text.
Node and edge attributes
Appearance is controlled by attributes written in square brackets. Attach them to a node by naming it with a bracket list, so a [shape=box, label="Start", color=blue] sets that node's shape, label, and colour. Attach them to an edge the same way after the edge, so a -> b [label="yes", style=dashed] labels and dashes that connection. The attribute vocabulary is large but memorable, and a handful covers most needs.
- shape: box, ellipse, circle, diamond, cylinder, and many more control a node's outline.
- label: the text shown; supports line breaks and, with the record shape, structured multi-field labels.
- color, fillcolor, and style=filled: set the outline colour and, when filled, the interior colour.
- style: dashed, dotted, bold, or rounded change how a node or edge is drawn.
- fontname, fontsize, and fontcolor: control typography for labels.
- arrowhead: on directed edges, choose normal, vee, diamond, dot, or none to signal the relationship type.
- Set defaults for all nodes or edges at once with a bare node [...] or edge [...] statement before the elements.
Layout direction and engines
By default Graphviz lays a directed graph out top to bottom, but you control the flow with the graph-level rankdir attribute. Set rankdir=LR at the top of the graph for a left-to-right layout, which suits wide process flows and pipelines far better than the default; TB, BT, and RL give the other three directions. The rank of a node - how far along the flow it sits - is computed automatically, but you can force nodes onto the same rank with a subgraph and rank=same when you need two things drawn side by side.
Graphviz is not one layout algorithm but several, exposed as different engines. The default, dot, produces the hierarchical layered layouts most people associate with Graphviz and is ideal for directed graphs like dependencies and flows. neato and fdp use force-directed placement for undirected relationship graphs, circo arranges nodes in a circle for cyclic structures, and twopi does radial layouts around a central node. Choosing the engine that matches your graph's nature - hierarchical, relational, cyclic, or radial - is the single biggest lever on how good the result looks.
Subgraphs, clusters, and real use
Structure comes from subgraphs. A subgraph whose name begins with cluster_ is drawn as a boxed, labelled grouping - subgraph cluster_frontend { label="Frontend"; a; b; } wraps nodes a and b in a titled rectangle, which is how you show subsystems, layers, or ownership boundaries. Non-cluster subgraphs do not draw a box but let you apply shared attributes or a rank=same constraint to a set of nodes. Clusters are the DOT equivalent of the packages and boundaries you use in other diagram languages.
Because DOT is text, it fits the same diagram-as-code discipline as PlantUML and Mermaid: keep the .dot file in the repository, review it in pull requests, and regenerate the image so it never drifts. Graphviz is especially well suited to generated graphs - many tools emit DOT because it is so simple to produce programmatically, which the guide at /guides/how-to-generate-diagrams-from-code explores. When you want a graph styled beyond DOT's automatic layout or edited collaboratively by non-technical colleagues, rebuild it in Atlas Diagram Studio at /diagrams, and see /guides/diagram-as-code-vs-gui-diagramming for when the text-first approach is worth it versus a visual editor.