@rse/gradia
v2.0.1
Published
Object Graph Diagram Rendering
Maintainers
Readme
Gradia
Object Graph Diagram Rendering
About
Gradia is a small API, MCP and CLI for rendering directed object graphs, described in a concise textual input language, as SVG diagrams. Eleven diagram types are supported which fit the scenario of visualizing object graphs:
- graph: a loosely grid-snapped layered layout of the whole graph.
- hub: a graph variant, placing one primary hub node between its input and output nodes.
- tree: a tree of nodes, connecting every parent with its children.
- sequence: a sequence diagram of actors and their messages.
- state: a state machine of states and transitions.
- petri: a Petri net of places, holding tokens, and transitions between the places.
- layer: a graph, stacking its node layers as horizontal bands.
- slice: a graph, lining up its node slices as vertical bands.
- matrix: a dependency structure matrix, counting the edges between its rows and columns.
- gantt: a Gantt chart of scheduled tasks and their dependencies.
- grid: an edge-less graph, consisting of a compact grid of nodes.
Examples
Here are examples of the graph diagram renderings:
sample-graph-regular (layered graph):
sample-graph-crowded (crowded channels with bundled edges):
sample-graph-simple (self-loops and edge arities):
sample-graph-container (nested containers):
sample-graph-gates (many edges crossing the container boundaries):
sample-graph-badges (node badges, multiple nodes, and word-wrapping):
sample-hub-regular (hub graph):
sample-hub-crowded (hub graph with wrapped input and output columns):
sample-tree-regular (tree with bus connectors and a cross link):
sample-tree-stacked (tree with stacked leaves and a cross link):
sample-tree-double (tree with partial and two-sided stacks and cross links):
sample-sequence-regular (sequence diagram):
sample-state-regular (state machine with unreachable and dead-end states):
sample-state-advanced (state machine with history, fork/join, junction, completion, and conflicting transitions):
sample-state-regions (state machine with a composite state of two orthogonal regions):
sample-petri-regular (Petri net of a producer, a bounded and inhibiting buffer, and two consumers):
sample-petri-violations (Petri net with a deadlock, a dead and a non-live transition, an unbounded place, and a two-way arc):
sample-layer-regular (layered architecture with clusters and violations):
sample-slice-regular (sliced architecture with clusters and violations):
sample-matrix-regular (acyclic dependency structure matrix in explicit order):
sample-matrix-full (dependency structure matrix with clusters, cycles, and badges):
sample-gantt-regular (Gantt chart with clusters, milestones, all dependency types, a lag, a lead, and the critical path):
sample-gantt-full (Gantt charts of two groups with nested clusters, explicit order, an earliest start, badges, and a named time unit):
sample-grid-regular (grid of tiles):
Playground
See playground.html for a demo playground.

Installation
$ npm install @rse/gradiaUsage
$ gradia [-t graph|hub|tree|sequence|state|petri|layer|slice|matrix|gantt|grid] [-f <format>] [-c <name>=<value>] -o <file>.svg <graph>.txtThe diagram type is selected with the -t command-line option or, when
this is absent, by the #type directive inside the input, and defaults
to graph.
The output format is svg:standalone (a standalone SVG/XML document,
the default), svg:embedded (the SVG without the <?xml?> declaration,
for direct embedding into HTML), url:xml (a data:image/svg+xml URL
with URL-encoded XML), or url:base64 (a data:image/svg+xml URL with
Base64-encoded XML).
MCP Service
$ gradia --mcp [-t graph|hub|tree|sequence|state|petri|layer|slice|matrix|gantt|grid] [-f <format>] [-c <name>=<value>]With the -m/--mcp option, Gradia instead runs as a Model Context
Protocol (MCP) service on stdio,
exposing a single tool gradia_render which renders a graph description
into an SVG diagram. The tool takes the arguments input (the graph
description), type, format, and config (an object of rendering
options), and returns the rendered SVG document or data URL as its text
result. Its description carries the entire input language grammar, so
an AI agent can author graph descriptions without further context. The
command-line options --type, --format, and --config act as the
defaults underlying the tool call arguments.
As the tool call arguments are treated as untrusted, the config
argument rejects the option font-embed and a font-family value
pointing to a WOFF2 file. Both remain available through the
(trusted) command-line options of the service.
Configure the service in an MCP client with:
{
"mcpServers": {
"gradia": {
"command": "npx",
"args": [ "-y", "@rse/gradia", "--mcp" ]
}
}
}Input Syntax
The input is a plain-text graph description: one node/edge chain per line, plus optional comments and rendering directives.
Grammar
The following BNF grammar describes the input syntax. Non-terminals are
written as <name>, terminals as "...", optional parts as [ ... ],
repeatable parts as { ... } (zero or more times), and alternatives are
separated by |.
<graph> ::= { <statement> | <newline> }
<statement> ::= <node> { <edge> <node> [ <edge-attrs> ] } ( <newline> | <eof> )
<node> ::= <atom> [ ":" <atom> ] [ <attributes> ]
<attributes> ::= "[" <attribute> { "," <attribute> } "]"
<edge-attrs> ::= "{" <attribute> { "," <attribute> } "}"
<attribute> ::= <atom> ":" <atom>
<edge> ::= "--" [ "(" <edge-name> ")" "--" ] ">" [ "[" <edge-arity> "]" ]
<edge-name> ::= <string> | <name-word>
<edge-arity> ::= <string> | <arity-word>
<atom> ::= <bareword> | <string>
<bareword> ::= <bareword-char> { <bareword-char> }
<bareword-char> ::= <char> - ( <whitespace> | "[" | "]" | "(" | ")" | "{" | "}" | "," | ":" | "\"" | ">" | "#" )
<name-word> ::= <name-char> { <name-char> }
<name-char> ::= <char> - ( <whitespace> | "(" | ")" | "\"" )
<arity-word> ::= <arity-char> { <arity-char> }
<arity-char> ::= <char> - ( <whitespace> | "[" | "]" | "\"" )
<string> ::= "\"" { <string-char> } "\""
<string-char> ::= ( <char> - ( "\"" | "\\" | <newline> ) ) | ( "\\" ( <char> - <newline> ) )
<comment> ::= "#" { <char> - <newline> }
<directive> ::= "#config" <whitespace> { <whitespace> } <option> <whitespace> { <whitespace> } ( <string> | <word> )
| "#type" <whitespace> { <whitespace> } <type>
<option> ::= <lowercase-letter> { <lowercase-letter> | <digit> | "-" }
<type> ::= "graph" | "hub" | "tree" | "sequence" | "state" | "petri" | "layer" | "slice" | "matrix" | "gantt" | "grid"
<word> ::= <char> - <whitespace> { <char> - <whitespace> }
<newline> ::= "\n"
<whitespace> ::= " " | "\t" | "\r"Lexical Rules
A
<bareword>must not contain the character sequence--at any position, as--always starts an edge operator.An
<edge>is a single lexical token: no whitespace and no comment is allowed anywhere inside it, especially not between>and[.Whitespace (except
<newline>) is insignificant between all other tokens and never part of a token.A
<newline>terminates a<statement>: statements cannot span multiple lines.A
<comment>starts at an unquoted#and extends to the end of the line. Inside a<string>,#is an ordinary character.A
<string>cannot span lines. Inside it,\escapes the following character (especially\"and\\).A
<directive>is a<comment>which occupies its entire line and starts with the keyword#configor#type. For#config, the<option>has to be one of the recognized rendering options (see below). All other comments are ignored.
Semantic Rules
In
<node>, the first<atom>is the node id and the optional<atom>after:is the displayed node label. Without a label, the id is displayed.A node can be referenced multiple times. All references address the same node and merge their label and attributes into it, with later values overriding earlier ones for the same attribute key.
A node id must not contain the NUL character (U+0000), as it is reserved for internal use: the diagram type
hub, and every diagram containing a container, reject such an id with an error.Nodes which are never part of an
<edge>are rendered free-standing.In a chain
a --> b --> c, each<edge>connects its immediately preceding and following node, i.e., the chain is equivalent to the two statementsa --> bandb --> c.The
<edge-name>is rendered as the label of the edge and the<edge-arity>as its cardinality label.The
<edge-attrs>following a node belong to the<edge>leading into that node, i.e., ina --> b { kind: async } --> cthey belong to the edgea --> b. The recognized keys arekind, with the valuessync(the default),async,return,create,destroy, andinhibitor,type, with the valuesfs,ss,ff, andsf, plusdurationandtime, with arbitrary texts. Only the diagram typesequencerenders the kindsasync,return,create, anddestroyand thedurationandtime, only the diagram typepetrithe kindinhibitor, and only the diagram typeganttthetypeand theduration(as a numeric lag, see below), while every diagram type rejects the ones it does not render with an error. Any other key is rejected with an error, too.Eleven attribute keys are reserved and consumed by the renderer instead of being displayed as a regular
<key>: <val>line:url: <url>renders the node box as a hyperlink. Only relative URLs and the schemeshttp,https, andmailtoare honored; any other URL is silently dropped and the node box then simply stays unlinked,type: <name>renders<name>as a smaller text above the node label inside the node box,primary: truerenders the node in the primary node colors (and, for diagram typehub, marks the single central hub node),group: <name>places the node into the surrounding group box<name>(every group being laid out as a diagram of its own, the groups stacked, and no edge running between them),cluster: <path>places the node into the nested clusters of the/-separated path<path>within the one diagram, crossed freely by the edges (for diagram typematrix: partitions of its rows and columns, for diagram typegantt: partitions of its rows, for diagram typelayer: groups of its layers, for diagram typeslice: groups of its slices), while the other diagram types ignore it,parent: <id>nests the node into the container node<id>(a node which is implicitly declared if never referenced otherwise),container: <type>marks the node as a container whose members are laid out with the diagram type<type>(graph,hub,grid, orstate), even if it has no members at all,order: <number>places the node explicitly (see below),badge: <icon>draws a round badge with an icon inside the node box, vertically centered at its left end, with the box widened to keep its text clear of it:person,team(two persons),bot(a robot head),device(a monitor),mobile(a phone),browser(a window),service(a gear),function(a lambda),api(a ball and socket),code(angle brackets with a slash),package(a cube),database(an upright cylinder),cache(a lightning bolt),queue(a lying cylinder),document(a page),mail(an envelope),communication(two-way arrows),clock,security(a shield),cloud, ornetwork(three linked nodes), an unknown icon being ignored,multiple: truedraws the node box as several instances: the front box with two copies behind it, offset towards the bottom-right,mark: unreachable|dead-end|incomplete|dead|unbounded|non-livemarks the node (or container) as a violation: its border is drawn in thecolor-mark-violationcolor, forunreachableanddeaddashed and with its outgoing edges marked, too. It is usually derived by diagram typestateorpetri, but honored by every diagram type drawing node boxes, unless the optionmark-violationisfalse.
Both
badgeandmultipleare ignored on a container node. Additionally, the diagram typeslayer,slice,state, andpetriconsume the attributelayer: <name>,slice: <name>,state: <kind>, andpetri: <kind>(plustokens: <count>andcapacity: <count>), respectively, and the diagram typeganttthe attributesduration: <number>andstart: <number>(see below), while all other diagram types display them as regular lines.A
graph(agraphcontainer level, too) with at least one node carrying anorderattribute is laid out in that explicit order instead of by the layered algorithm: the distinct order values become the rows, top-down in ascending order, the nodes of one value fill their row left-to-right in declaration order (wrapped into further rows beyondgraph-columns-maxnodes), and the nodes without an order follow in the trailing rows. Agridplaces its tiles in the order of the attribute, too, the tiles without one trailing. An invalid value is ignored.A node cannot be a member of more than one group. As soon as at least one node carries a
groupattribute, all nodes without one implicitly belong to the groupdefault, and every edge has to stay within a single group. A nested node belongs to the group of its outermost container.A node referenced as the
parentof another node is a container: it is rendered as a dashed grey box surrounding its members instead of as a node box, with its label,type, andurlin the box head (its remaining attributes are not rendered). Containers nest to any depth, but a node cannot be a member of more than one container, and cannot be nested into itself.The members of a container are laid out as a diagram of their own, with the diagram type of the enclosing level unless overridden by
container: <type>, and the container then takes part in the layout of the enclosing level as a single box. Edges may connect any two nodes, across container boundaries and to container nodes themselves, except a container with one of its own members. An edge crossing the boundary of a container laid out asgraph(orstate) is routed through the box border to the inner node (entering on the west and leaving on the east side, and where other nodes lie between the inner node and the border, around them through the gutter below, passing the border at the height of that gutter), while an edge crossing the boundary of a container laid out ashuborgridends at the box border instead.All remaining attributes are displayed as
<key>: <val>lines inside the node box.
Diagram Types
Common Rules
Groups and containers: the groups of a grouped graph, and the members of every container, are laid out individually with the selected (or, for a container, the annotated) diagram type. Hence the constraints of a diagram type apply to every single group and container level.
Boundary-crossing edges: an edge crossing a container boundary counts at the level it is lifted to. The boundary-crossing edges of a
graphcontainer additionally count inside it, from the boundary to the inner node.Containers: only the diagram types
graph,hub,grid, andstatelay out the members of a container. The diagram typeslayer,slice,tree,matrix,sequence,petri, andganttreject the input with an error as soon as the graph contains a container.Violations: the diagram types
layer,slice,tree,matrix,state, andpetrimark their violations in thecolor-mark-violationcolor, unless the optionmark-violationisfalse, and the diagram typeganttits critical path in thecolor-mark-criticalcolor, unless the optiongantt-criticalisfalse.
Diagram Type graph
The diagram type graph imposes no constraints on the topology: it
accepts self-loops and free-standing nodes alike, and hence is the safe
choice whenever the topology is not known to fit hub or grid.
Diagram Type hub
The diagram type hub rejects the input with an error unless all of the
following conditions hold:
Exactly one node carries the attribute
primary: true. Hence every group and everyhubcontainer needs its own primary node.Every edge either points to or originates from that primary node. An edge between two non-primary nodes is rejected.
Every non-primary node is an input or an output of the primary node, i.e., no node is free-standing.
Two special placements extend the layout:
Ghost: a node which is both an input and an output of the primary node is placed twice, once in the input column and once in the output column. The second placement is rendered as a dashed "ghost" box, colored by the
color-node-ghost-*options.Self: a self-loop on the primary node is unrolled: the primary node is placed a second time on top of the output column, as the target of the self-loop. This second placement is rendered as a dashed "self" box, colored by the (darker grey)
color-node-self-*options.
Diagram Type grid
The diagram type grid rejects the input with an error as soon as the
graph contains at least one edge.
Diagram Type layer
The diagram type layer lays out a layered architecture:
Layers: the attribute
layer: <name>assigns a node to a layer. The layers stack top-down in the order of their first appearance, as bands spanning the whole diagram (colored by thecolor-container-*options, the layer name left of its nodes).Nodes: the nodes of a layer fill its band left-to-right (wrapped beyond
graph-columns-maxnodes), while the nodes without a layer trail outside of any band. The layers determine the order of the rows, so anorderattribute of a node is ignored.Boxes: all node boxes take the width of the widest one and the boxes of a layer the height of the highest one, and the boxes of a layer stay aligned in its band.
Violations: an edge pointing from a lower layer to a higher one, i.e., against the layering, is drawn dashed in the
color-mark-violationcolor.Clusters: the attribute
cluster: <path>assigns the layer of a node to nested clusters (/-separated, outermost first, e.g.Server/Broker). They are drawn as columns left of the bands, spanning the bands of their layers, with their names running upwards. The layers of a cluster are kept together, all nodes of a layer have to name the same path (otherwise the input is rejected with an error), and the edges may freely cross the clusters.Numbering: the boolean option
layer-numberednumbers the layers bottom-up (by default, as every layer builds on the ones below it).
Diagram Type slice
The diagram type slice is the companion of the diagram type layer
and lays out a sliced architecture:
Slices: the attribute
slice: <name>assigns a node to a slice. The slices line up left-to-right in the order of their first appearance, as bands spanning the whole diagram (colored by thecolor-container-*options, the slice name above its nodes), and are never wrapped, regardless ofgraph-columns-max.Nodes: the nodes of a slice stack top-down in its band in declaration order (never wrapped), while the nodes without a slice trail outside of any band. The slices determine the order of the columns, so an
orderattribute of a node is ignored.Boxes: all node boxes take the width of the widest one and the boxes of a row the height of the highest one. The channels between the bands widen (within a few re-layouts) until the edges within a slice stay inside its band.
Violations: an edge pointing from a right slice back to a left one is drawn dashed in the
color-mark-violationcolor if it closes a direct cycle, i.e., if any edge points from that left slice to the right one. Transitive cycles are not checked.Clusters: the attribute
cluster: <path>assigns the slice of a node to nested clusters, which are drawn as rows above the bands, spanning the bands of their slices (with the same rules as for the diagram typelayer).Numbering: the boolean option
slice-numberednumbers the slices left-to-right.
Diagram Type tree
The diagram type tree draws a tree, or a forest of trees:
Structure: every edge points from a parent to its child. The first edge into a node makes its source the parent of the node, unless it would close a cycle. The nodes without a parent are the roots, and several roots form a forest of trees side by side.
Order: the children of a parent, and the roots, keep their declaration order, or follow their
orderattributes.Levels: the levels stack top-down (or, with the boolean option
tree-horizontal, left-to-right), all boxes of a level aligned at its start and the levelstree-level-gapapart. Every parent sits centered over its children, and the subtrees are packed as tightly astree-sibling-gapallows (a "tidy tree").Connectors: a parent connects to its children through a bus: a trunk out of the parent, a bar across its children, and a drop into every child, which carries the arrow head, name, and arity of the edge. A single child is connected directly.
Stacking: the leaf children of a parent are stacked along a trunk, instead of side by side, as soon as the parent has at least
tree-stack-minof them (default4), or their level holds more thantree-row-maxnodes (each option only if positive). If all children of the parent are stacked, the trunk leaves the parent box itself, otherwise it hangs off the bus of the parent, at the position of the first stacked leaf among its other children. With the boolean optiontree-stack-double, the stacked leaves alternate between both sides of the trunk, which halves the height of the stack.Cross links: every other edge (a second edge into a node, an edge closing a cycle, or a self-loop) is a cross link, drawn dashed, and in the
color-mark-violationcolor unlessmark-violationisfalse. It runs through the gutters in front of the levels, on its own tracks, and between different levels along a lane of its own beside the nearer side of the tree, so it never crosses a node box (a stacked node attaches through a corridor beside its stack, and a level whose gutter carries such tracks starts below the stacks above it).Node width: unless configured otherwise, the diagram type
treeuses a minimum node box width (size-node-width-min) of160and a maximum one (size-node-width-max) of240, so the node texts wrap and the tree stays narrow. With the boolean optiontree-node-width-equal, all node boxes of a level take the width of its widest one.
Diagram Type matrix
The diagram type matrix draws a dependency structure matrix (DSM):
Headers: the nodes are both its rows and its columns, their headers carrying the node labels (linked through
url), all row headers as wide as the longest label and all column headers as high. The type shared by all rows, and the one shared by all columns, become their titles in the corner.Badges: a node
badgeis drawn inside its headers, at the end their labels start from (left of a row label, below a column label), like inside a regular node box. As soon as one node carries a badge, all labels are set back by its room, so they stay aligned.Cells: the cell of row X and column Y counts the edges from X to Y (an edge whose arity is a plain non-negative integer counting as that number, every other edge as one), and the diagonal is shaded.
Order: the nodes are ordered topologically, the sources of the edges before their targets (and the declaration order deciding among the independent ones), so all entries lie above the diagonal, except the ones of the cycles. As soon as one node carries an
orderattribute, the nodes are instead ordered by these attributes (the nodes without one trailing, and the members of a cycle still kept consecutive).Violations: every cycle is kept in consecutive rows and outlined as a dashed block on the diagonal, with the numbers of its edges closing it (the ones pointing back to an earlier or the same node, on or below the diagonal) in the
color-mark-violationcolor.Clusters: the clusters (see the
clusterattribute) become partitions of the matrix: consecutive rows and columns, each ordered on its own, labeled by bands left of the rows and above the columns, and outlined as a block on the diagonal. The clusters follow the order of their first appearance, the nodes without a cluster belong to the implied clusterdefault, and a/-separated cluster path nests sub-clusters, kept together within their cluster, with one level of bands and blocks per nesting level.Cross-cluster edges: the edges between the clusters are counted outside the cluster blocks. A cycle spanning several clusters is outlined per cluster, but only where its members within that cluster have an edge among themselves, as an empty block would suggest a local cycle which only closes through other clusters. Otherwise only the counts of its edges mark the cycle.
Diagram Type sequence
The diagram type sequence draws a sequence diagram:
Participants: the participants (the nodes) are ordered by declaration or by their
orderattributes (left-to-right in ascending order, the nodes without one following). Their boxes keep their own widths, but share the height of the highest one. The dashed lifelines are colored by thecolor-node-lifelineoption.Messages: the messages (the edges) run top-down in declaration order, and a message from a participant to itself loops on its own lifeline. The
kindof an edge selects the kind of its message: a synchronous call (sync, the default) is drawn as a solid line with a filled arrow head, an asynchronous message (async) as a solid line with a hollow arrow head, and a return (return) as a dashed line with a hollow arrow head.Activations: a return answers the latest still unanswered synchronous call from its sender back to its receiver, and such an answered call activates its callee until its return, drawn as an activation bar on top of the lifeline (nested and shifted to the right for a call arriving at an already active participant). A caller without an activation of its own gets an implicit one, held until all of its calls have returned. Unanswered calls and asynchronous messages activate nothing. The activation bars are colored like the regular node boxes.
Creations: a creation (
create) is drawn as a dashed line with a hollow arrow head, ending at the box of the participant it creates, which is placed onto that message instead of the top row, with its lifeline starting below it. A creation counts as a call, i.e., a return answering it activates the created participant.Destructions: a destruction (
destroy) is drawn as a solid line with a filled arrow head, ending at a cross on the lifeline of its receiver, which ends there (together with all of its activations). A destruction from a participant to itself is drawn as its cross only (without its name and arity).Lifecycle: a participant can only be created by the first message it takes part in, and cannot take part in any message after its destruction, otherwise the input is rejected with an error.
Durations: a
durationof a call (or creation) is a duration constraint up to its return, drawn as a bracket beside the lifeline of the caller (on the side facing away from the callee), spanning from the call to the arrival of the return answering it, with the given text beside it. A call without such a return cannot carry aduration, otherwise the input is rejected with an error.Times: a
timeof any message is a time constraint or observation at its start, drawn as the given text beside the lifeline of its sender (on the side facing away from its receiver).
Both texts of duration and time are drawn exactly as given, e.g.,
"{0..200ms}" or "t=now".
Diagram Type state
The diagram type state draws a state machine.
Layout: the states (the nodes) and transitions (the edges) are laid out like a
graph, but with up to 8 instead of 4 side-by-side nodes (unlessgraph-columns-maxis configured), and every transition is drawn on its own (never bundled).Transitions: the name of a transition is its label, e.g.
"evt [guard] / action"(quoted, as it contains whitespace). The guard is drawn in italics and the action dimmed. The guards of the transitions leaving a choice or junction are placed at their start.States: the attributes of a state (e.g. its behaviors
entry,do, andexit) are drawn below a line dividing them from its name.Pseudo-states: the attribute
state: <kind>turns a node into a pseudo-state, drawn without its texts in thecolor-node-pseudo-*colors:initial: a filled dot,final: a bullseye,terminate: a cross,choice: a diamond,junction: a small dot,forkandjoin: a bar, growing with its edges,historyanddeep-history: a circledHandH*(inside a composite state only),entry-pointandexit-point: a hollow and a crossed-out circle (inside a composite state only).
Composite states: containers act as composite states. They are drawn as solid grey boxes, bordered in the
color-composite-bordercolor, with their name and their attributes above a dividing line.Regions: the attribute
region: <name>splits a composite state into orthogonal regions, active concurrently: its members sharing a name form one region. The regions are stacked top-down as bands (in the order of their first appearance), separated by dashed lines, each with its name in its top-left corner.Semantics: a transition into a composite state enters its initial state (in each of its regions), and one into its history state resumes it. A transition out of a composite state leaves any of its members. An unlabeled transition out of it is a completion transition, firing only once an inner final state is reached (in each of its regions). A join fires once all of its sources are reached.
Validation: the input is rejected with an error if
- a node with a
regionis not inside a composite state, - a composite state mixes members with and without a
region, - a transition crosses between two regions of a composite state,
- an initial state has an incoming transition,
- a final or terminate state has an outgoing transition,
- a choice, junction, fork, join, or entry point has no outgoing transition,
- a history state or join has more than one outgoing transition,
- a fork has more than one incoming transition,
- a history state, entry point, or exit point is not inside a composite state,
- a history state without a default transition is inside a composite state without an initial state,
- a containment level (or the top level) has more than one initial (or history, or deep-history) state,
- a transition enters a composite state without an initial state (in each of its regions), or
- a pseudo-state has members.
- a node with a
Analysis: the following violations are marked in the
color-mark-violationcolor (unless the node carries an explicitmarkattribute already):unreachable(dashed border and outgoing transitions): a state not reachable from the top-level initial state (only checked once the top level has an initial state),dead-end(solid border): a regular state which is trapped, i.e. belongs to a group of states no transition leads out of and which holds no final or terminate state, e.g. a single state without a transition leading out of it (a self-loop not counting), or two states only leading into each other,incomplete(solid border): a choice or junction whose outgoing transitions are all guarded, but none by[else], or a composite state with a final state but no completion transition,- conflicting transitions (dashed): the transitions leaving the same state (except a fork) on the same event with the same (or no) guard.
Diagram Type petri
The diagram type petri draws a Petri net (a place/transition net).
Layout: the places and transitions (the nodes) and the arcs (the edges) are laid out like a
graph, but with up to 8 instead of 4 side-by-side nodes (unlessgraph-columns-maxis configured), a minimum node box width (size-node-width-min) of120instead of220and a minimum channel width (graph-channel-width-min) of80instead of24(unless configured), and every arc drawn on its own (never bundled). The layout starts at the tokens: the flow reads left-to-right from the initially marked places (and the transitions without input places), while the arcs returning to them point backwards, so the layout is independent of the declaration order.Places: a place is drawn as a circle in the
color-node-place-*colors around its name (lighter than a node name, in the smallersize-font-propfont size), its attributes (in thesize-font-typefont size), and its initial tokens, given as the attributetokens: <count>(a non-negative integer,0by default), drawn as dots (arranged like the faces of a die, two tokens side by side) or, beyond 6 tokens, as their count. The optional attributecapacity: <count>(a positive integer) limits the tokens the place can hold and is drawn in the usual notationK = <count>. A sink place, i.e. a place with incoming but without outgoing arcs, where the tokens come to rest, is drawn as a double circle.Transitions: a transition is drawn as a regular node box, with its attributes (e.g. a
delay) as regular attribute lines.Kinds: the attribute
petri: placeorpetri: transitionsets the kind of a node, and atokensorcapacityattribute implies a place. As every arc connects a place with a transition, the kinds of all other nodes follow from their arcs, and a connected part of the net without any such node starts with its first declared node as a place. Hence, annotating the transitions, or giving the initial tokens, usually suffices.Arcs: the arity of an arc, e.g.
-->[2], is its weight, i.e., the number of tokens it consumes or produces (1by default), while its name is a mere label. Parallel arcs add up their weights. A pair of opposite arcs between a place and a transition, both without a name and of the same weight (e.g. a place merely tested by a transition), is drawn as a single arc with an arrow head at both ends (and its weight drawn once).Inhibitor arcs: an arc of the kind
inhibitor, e.g.p -->[3] t { kind: inhibitor }, leads from a place to a transition and is drawn ending in a small disc instead of an arrow head. It consumes nothing, but disables its transition as long as its place holds at least as many tokens as its weight (parallel inhibitor arcs taking the lowest weight).Semantics: a transition is enabled if each of its input places holds at least as many tokens as the weight of its arc, each of its inhibiting places fewer tokens than the weight of its inhibitor arc, and each of its output places stays within its capacity after firing. Firing it consumes the weighted tokens from its input places and produces the weighted tokens into its output places. Hence, a transition without input places is enabled as long as its inhibiting and output places permit it.
Validation: the input is rejected with an error if
- an arc connects two places or two transitions,
- a
petriattribute is neitherplacenortransition, - a transition carries a
tokensorcapacityattribute, - an inhibitor arc leads from a transition to a place,
- a
tokens,capacity, or arc weight is not an integer in its range, or a place carries more tokens than its capacity, or - the net contains a container.
Analysis: once the net is not inert, i.e., carries an initial token or a transition without input places, every connected part of it is analyzed on its own through its coverability graph (the algorithm of Karp and Miller, which explores the markings reachable from the initial one, and lets the tokens of a place become infinite once a marking covers one of its predecessors with more tokens in that place). A part with inhibitor arcs is explored without this acceleration (as it is not sound for them), so its unbounded places are never proven, but exhaust the budget (see below) instead.
Tooltips: the analysis results of every node are attached to it as its tooltip (shown when hovering over it in a browser): for a place its initial and its most tokens (or that they grow without bound, with the firing sequence to repeat for it and the one leading there), the shortest firing sequence reaching a deadlock it holds a token in, and whether its capacity is never reached, and for a transition whether it never fires, fires, or is live (can always fire again, from every reachable marking).
Violations: the following violations are marked in the
color-mark-violationcolor (unless the node carries an explicitmarkattribute already):unreachable(dashed border and outgoing arcs): a place which never holds a token,dead(dashed border and outgoing arcs): a transition which never fires,dead-end(solid border): a place holding a token in a deadlock, i.e., a marking enabling no transition, while feeding a transition (so a token resting in a place without outgoing arcs is a proper termination),unbounded(solid border): a place whose tokens grow without bound,non-live(solid border): a transition which fires, but can get disabled forever, as it fires nowhere within a recurring group of markings the net cannot leave again (a terminal strongly connected component of the reachability graph holding a firing, e.g. a loop entered after an initialization transition), only checked for a completely explored bounded part (a reachable deadlock is marked through itsdead-endplaces instead).
The analysis works within a budget shared by all parts of the net (at most 10000 explored markings and a bounded number of comparisons of markings against their predecessors), so the rendering of a large or hostile input stays fast. A part explored only partially within it keeps its unreachable places and dead and non-live transitions unmarked, while its found unbounded and deadlocked places are still marked. For an unbounded part, the coverability graph exhibits only some of its deadlocks.
Diagram Type gantt
The diagram type gantt draws a Gantt chart of a project plan.
Tasks: the tasks (the nodes) are drawn as one row each, with a row header like the ones of a
matrix(the label, linked throughurl, abadgein front of it, the primary colors, and the type shared by all tasks as the title in the corner) and a bar over a time axis of abstract numeric units (with its ticks and their faint grid lines). The attributeduration: <number>is the length of a task andstart: <number>its earliest start (both non-negative numbers,0by default). A task without a duration is a milestone, drawn as a dark grey diamond (in thecolor-node-pseudo-*colors). The optiongantt-unit-width(40by default) sets the width of one time unit, and the optiongantt-unit-name(e.g.days) its name, shown above the row headers. The schedule of every task (its earliest and latest start and finish, and its slack) is attached to its bar as its tooltip.Dependencies: the dependencies (the edges) are drawn as orthogonal arrows between the bars. The edge attribute
typeis the type of a dependency:fs(finish-to-start, the default),ss(start-to-start),ff(finish-to-finish), orsf(start-to-finish), and the edge attributeduration: <number>its lag (a negative one being a lead), e.g.a --> b { type: ss, duration: 2 }startsbat least 2 units after the start ofa. A lag or lead is labeled at the start of its arrow (e.g.+2or−2). Arrows sharing the start or end of a bar run on tracks of their own, and every arrow runs its vertical segment where it crosses the fewest bars. The edge names and arities are ignored.Scheduling: the tasks are scheduled as early as possible by the critical path method: a forward pass determines the earliest start of every task (the latest of its
startand of what its dependencies imply), and a backward pass from the project end (the latest earliest finish) its latest finish. The slack of a task, i.e. how far it can slip without delaying the project, is drawn as a dashed whisker after its bar, up to its latest finish.Critical path: every task without slack, and every dependency among them determining the earliest start of its target, form the critical path, drawn in the
color-mark-criticalcolor (a critical milestone ringed in it), unless the optiongantt-criticalisfalse.Order: the rows follow the earliest start of their tasks (ties in declaration order). As soon as one task carries an
orderattribute, the rows instead follow these attributes (the tasks without one trailing).Clusters: the clusters (see the
clusterattribute) become partitions of the rows, exactly like for amatrix: consecutive rows, each partition ordered on its own, labeled by bands left of the row headers, and outlined across the chart.Validation: the input is rejected with an error if
- a
durationorstartis not a non-negative number, - a lag is not a number,
- the dependencies form a cycle (naming the tasks on it), or
- the chart contains a container.
- a
Directives
A #type directive sets the diagram type (graph, hub, tree,
sequence, state, petri, layer, slice, matrix, gantt, or grid), which otherwise is
settable through the -t/--type command-line option (which takes
precedence).
The last occurrence wins and an invalid type is silently ignored.
Without either, the type defaults to graph.
A #config directive sets one of the rendering options, which otherwise
are settable through the corresponding --config <option>=<value>
command-line options (font-embed being command-line only, see below),
which take precedence. A directive with an unknown option or an invalid
value is silently ignored, while the command-line option rejects it
with an error.
The recognized options, and their default values, are:
font-family Helvetica size-canvas-margin 40
font-embed false size-node-width-min 220
color-node-regular-name #336699 size-node-width-max 0
color-node-regular-box #e0f0ff size-node-height-scale 2.25
color-node-regular-border #c0d0e0 size-edge-corner-radius 20
color-node-primary-name #ffffff size-edge-hop-radius 8
color-node-primary-box #336699 size-edge-track-gap 12
color-node-primary-border #003366 size-edge-port-gap 24
color-node-ghost-name #666666 group-box-padding 30
color-node-ghost-box #f0f0f0 group-box-gap 40
color-node-ghost-border #a0a0a0 container-box-padding 30
color-node-self-name #444444 graph-columns-max 4
color-node-self-box #e0e0e0 graph-channel-width-max 140
color-node-self-border #707070 graph-channel-width-min 24
color-node-lifeline #c0d0e0 graph-gutter-height-max 90
color-group-name #6699cc graph-gutter-height-min 20
color-group-box #f4f8fc graph-node-separation 30
color-group-border #c0d0e0 graph-rank-separation 60
color-container-name #666666 graph-node-degree-max 3
color-container-box #f6f6f6 hub-channel-width-max 340
color-container-border #a0a0a0 hub-channel-width-min 240
color-edge-line #999999 hub-node-gap 20
color-edge-name #333333 hub-node-degree-max 3
color-edge-arity #333333 hub-node-count-max 0
color-edge-halo #ffffff grid-columns-max 4
size-font-node 30 grid-columns-min 3
size-font-type 16 grid-gap-horizontal 40
size-font-prop 22 grid-gap-vertical 20
size-font-edge 16 grid-node-width-equal true
size-font-arity 16 grid-node-height-equal true
color-mark-violation #b06820 sequence-column-gap 20
color-mark-critical #4a88c7
color-composite-border #c0c0c0
color-node-pseudo-name #ffffff
color-node-pseudo-box #707070
color-node-pseudo-border #444444
color-node-place-name #555555
color-node-place-box #ececec
color-node-place-border #c0c0c0
sequence-message-gap 40
tree-level-gap 60
tree-sibling-gap 20
tree-row-max 0
tree-stack-min 4
tree-stack-double false
tree-node-width-equal false
tree-horizontal false
gantt-unit-width 40
gantt-unit-name ""
gantt-critical true
mark-violation true
layer-numbered true
slice-numbered falseThe size-*, group-*, container-*, graph-*, hub-*, grid-*,
sequence-*, tree-*, and gantt-* options control the rendering geometry (text
font sizes, canvas margin, node box sizing, edge routing, group and
container box spacing, and the per-diagram-type layout) and take
non-negative numbers, except the booleans grid-node-width-equal,
grid-node-height-equal, tree-stack-double, tree-node-width-equal,
tree-horizontal, and gantt-critical, and the string gantt-unit-name.
The boolean mark-violation option controls whether the diagram types
matrix, layer, slice, tree, state, and petri mark their
violations at all (in the color-mark-violation color), and whether a mark
attribute is honored: by default they do, while with false they render
the very same diagram without any marking (the cross links of a tree
stay dashed, though). Likewise, the boolean gantt-critical option
controls whether the diagram type gantt marks its critical path (in the
color-mark-critical color).
The size-font-* options control the font sizes of the node names
(size-font-node), the node types (size-font-type), the node
properties (size-font-prop), the edge names (size-font-edge), and
the edge arities (size-font-arity). They are geometry options, as they
size the node boxes and the channels the edge labels land in: the line
heights inside a node box, the head of a container box, and the width
demand of a channel all track their font size.
The size-node-width-max option additionally enables the word-wrapping
of the node box texts: given a positive value, the node name, its type,
and its attribute lines are greedily broken at whitespace so that the
node box stays within the given width and grows in height instead. The
value 0 disables the wrapping entirely and hence lets a node box
become as wide as its longest text. A single word wider than the
maximum is never broken and still widens the box beyond the maximum.
The group-box-padding and container-box-padding options control the
inner padding of the group and container boxes around their content.
For a graph container, the content additionally includes the
boundary channels along its west and east sides, through which the
edges crossing the container boundary reach the inner nodes, sized
like all channels by the edges routed through them.
The size-edge-port-gap option controls the distance of the edge
attachment ports along a node side, and hence the vertical distance of
the parallel edges and their labels. A node box grows in height until
it holds all of its ports at that distance, so the edges of a node stay
readably apart however short its textual content is, and a node side
carrying more edges than graph-node-degree-max or
hub-node-degree-max grows by this very distance per additional edge
on top of that. In a diagram whose edges carry arities, the distance is
raised to what those labels demand, as an arity sits beside its own
edge line and has to stay clear of the arrow head above it. Only the
fixed-size box of a container placeholder cannot grow and hence packs
its ports closer together instead.
The graph-channel-width-* and graph-gutter-height-* options control
the spacing of a graph diagram: its inter-column channels and
inter-row gutters are sized by the edges routed through them and by the
edge labels landing inside them -- a channel by the widest label demand
it has to hold (an edge name placed along it, or an edge arity set back
from its arrow head), a gutter by one text line per edge name placed
along its run. The -min options raise the result, so that even an
unlabeled diagram can be spaced out, while the -max options cap it, so
that neither many parallel edges nor a long label can push the nodes
apart without bound. Only a crowded channel or gutter, whose edges are
bundled, grows beyond its -max cap as far as its bundled tracks need.
In a graph diagram (a graph container level, too) whose nodes end up
in a single column, the container boxes are widened to the widest one,
so the stacked boxes line up: their content stays centered, and their
height remains the one of their content. With more than one column,
every container box keeps the width of its own content.
The hub-node-count-max option controls the wrapping of the input and
output column of a hub diagram: given a positive value, a column of
more nodes than this wraps into two staggered sub-columns, whose nodes
alternate between the outer sub-column and the inner one, with every
outer node vertically centered onto a gap between two inner nodes,
through which its edges reach the primary node. This trades nearly
half of the height of a large diagram for additional width. The option
decides for the larger of the two columns alone, as the height of the
diagram follows that one: the node count of its tallest resulting
(sub-)column implicitly caps the smaller column, which hence wraps
exactly if that lowers the height of the diagram. The value 0
disables the wrapping entirely.
The grid-columns-min and grid-columns-max options control the column
count of a grid diagram, which by default is derived from the node
count as a roughly square grid: grid-columns-min raises the derived
count, so that a few nodes still share a single row instead of being
stacked, while grid-columns-max caps it, so that larger diagrams grow
in height only. The column count never exceeds the node count and the
maximum always wins over the minimum.
The grid-node-width-equal and grid-node-height-equal options control
the tile sizes of a grid diagram: the value true forces all node
boxes to the width of the widest one (or the height of the tallest one)
and hence yields a strictly regular grid, while false lets each column
become only as wide as its own widest node box (or each row only as tall
as its own tallest node box), with the tiles left-aligned within their
column (or top-aligned within their row). A nested container box takes
part in the width unification only, as its height is the one of its
content: it never grows to the height of the tallest tile of its row,
but keeps its row as tall as itself.
The sequence-column-gap and sequence-message-gap options control the
spacing of a sequence diagram: the former is the minimum horizontal
gap between two neighboring participant boxes, the latter the vertical
gap between two consecutive messages. Every participant box keeps its
own width, and every pair of neighboring lifelines is spaced on its
own, i.e., it is widened beyond that minimum only as far as the names
of the messages between them demand, so neither a single wide box nor
a single long message name widens the whole diagram.
The font-family and color-* options are embedded directly into the
generated SVG. When such an option is not explicitly configured, the
SVG references the CSS custom property --gradia-<option> (definable by
the embedding document, e.g. for the svg:embedded output format) and
falls back to the built-in default. An explicitly configured value is
hard-coded into the SVG instead. The resulting precedence is: first
#config/--config, then the CSS custom property --gradia-<option>,
and finally the built-in default.
The size-font-* options are embedded into the generated SVG, too, but
as they also size the layout, the SVG always references the CSS custom
property --gradia-<option> and falls back to the effective value
(configured or default). An override at display time hence retains the
layout of the configured size and should stay close to it.
The styling is not repeated on every element of the generated SVG, but
declared once as CSS classes in a <style> element, which the elements
reference. A class is named gradia-<digest> after the digest of its
own declarations, so equal declarations always yield the very same
class. As the class names are global once multiple diagrams are embedded
into the very same document, the embedding document can hence strip the
<style> elements off the diagrams and declare the union of their
rules (one per line) a single time on its own.
The font-family option is either a built-in font family, a plain font
family name, or the path to a WOFF2 file. The only built-in font family
is Source Sans 3, shipped as a variable WOFF2 file by the NPM
dependency source-sans,
and hence the only font which can be embedded without an external font
file:
$ gradia -c font-family="Source Sans 3" -c font-embed=true -o graph.svg graph.txtThe option font-embed, and a font-family value pointing to a WOFF2
file, are intentionally rejected in directives, as the input is treated
as untrusted. Both are available on the command line only.
Example
#type graph
#config color-node-primary-box #336699
#config color-edge-line #999999
Animal: animal [ type: "abstract class", kind: base, url: "#animal", primary: true ]
Dog: dog
Cat: cat
Dog --(isa)--> Animal
Cat --(isa)--> Animal
"Pet Owner" --(owns)-->[0..*] Dog --(chases)-->[0..n] CatLicense
Copyright © 2026 Dr. Ralf S. Engelschall (http://engelschall.com/) Distributed under MIT license (https://spdx.org/licenses/MIT.html)
