Getting Started

Plotting requires loading a Makie backend and GraphMakie.jl. Below we use CairoMakie, and Graphs.jl to build graphs:

using Sugiyama
using Graphs
using CairoMakie
using GraphMakie

SugiyamaLayout follows the same "callable struct" convention as NetworkLayout.jl, so it can be passed directly as the layout keyword to graphplot.

Laying out an adjacency matrix

sugiyama (and the SugiyamaLayout callable) take the adjacency matrix of a directed graph and return one coordinate per vertex, as a Vector{Point{2,Float64}}:

adj = [0 1 1 0;
       0 0 0 1;
       0 0 0 1;
       0 0 0 0]

positions = sugiyama(adj)
4-element Vector{GeometryBasics.Point{2, Float64}}:
 [0.0, 1.0]
 [2.0, 0.0]
 [2.0, 2.0]
 [4.0, 1.0]

Vertex i of adj corresponds to positions[i]. Here vertex 1 has two children (2 and 3), which both point to vertex 4, so vertices 2 and 3 land on the same rank, side by side, above vertex 4:

g = SimpleDiGraph(adj)
graphplot(g; layout = SugiyamaLayout(), ilabels = repr.(1:nv(g)))

Using a Graphs.jl graph

An AbstractGraph can be passed directly instead of an adjacency matrix, to sugiyama/SugiyamaLayout as well as to graphplot:

sugiyama(g)
4-element Vector{GeometryBasics.Point{2, Float64}}:
 [0.0, 1.0]
 [2.0, 0.0]
 [2.0, 2.0]
 [4.0, 1.0]

Customizing the layout

Both sugiyama and SugiyamaLayout accept keyword arguments to control node spacing, direction, and the ranking/crossing-minimization heuristics used internally. For example, laying out top-to-bottom instead of left-to-right:

graphplot(g; layout = SugiyamaLayout(; direction = :down), ilabels = repr.(1:nv(g)))

See the Reference for the full list of keyword arguments.

Cycles

Cycles are broken internally (by implicitly reversing edges) so that any directed graph can be laid out, not just DAGs:

cyclic = SimpleDiGraph(3)
add_edge!(cyclic, 1, 2)
add_edge!(cyclic, 2, 3)
add_edge!(cyclic, 3, 1)

graphplot(cyclic; layout = SugiyamaLayout(), ilabels = repr.(1:nv(cyclic)))

Disconnected graphs

Weakly connected components are laid out independently and placed side by side:

disconnected = SimpleDiGraph(4)
add_edge!(disconnected, 1, 2)
add_edge!(disconnected, 3, 4)

graphplot(disconnected; layout = SugiyamaLayout(), ilabels = repr.(1:nv(disconnected)))

Edge routing with CausalStructures

using CausalStructures

Here we use CausalStructures.jl, for generating and plotting a DAG:

dag = DAG("A --> X, A --> B, X --> Y, B --> Y, A --> Y")

ns = nodes(dag)
node_index = Dict(n => i for (i, n) in enumerate(ns))

adj = zeros(Int, length(ns), length(ns))
for e in CausalStructures.edges(dag)
    adj[node_index[e.src], node_index[e.dst]] = 1
end

positions = sugiyama(adj)
plot(dag; layout = positions)

CausalStructures does automatic edge routing via Bezier curves if needed, but instead of falling back to that, we can use sugiyama_paths for the routing sugiyama uses. Alongside the positions, it returns edge_paths, a Dict mapping each (i, j) edge to the polyline it should be drawn as: positions[i], then one bend point per rank the edge spans, then positions[j]:

positions, edge_paths = sugiyama_paths(adj)
edge_paths[(node_index[:A], node_index[:Y])]
3-element Vector{GeometryBasics.Point{2, Float64}}:
 [0.0, 2.0]
 [2.0, 3.5]
 [4.0, 2.0]

plot accepts this directly as its own edge_paths keyword (keyed by node name rather than index here):

name_of = Dict(i => n for (n, i) in node_index)
named_paths = Dict((name_of[i], name_of[j]) => path for ((i, j), path) in edge_paths)

plot(dag; layout = positions, edge_paths = named_paths)