Skip to main content

A fast and flexible hierarchical graph layout engine.

Project description

Fast-Sugiyama

codecov Rust Lint & Test Python Lint & Test Package version

This project adds python bindings for the rust-sugiyama crate, to produce directed graph layouts similar to the GraphViz dot program.

It's an implementation of Sugiyama's algorithm for displaying a layered, or hierarchical, directed graph like so:

Graph Example

Why?

We already have nx.nx_pydot.pydot_layout(gn, prog="dot"), grandalf.layouts.SugiyamaLayout, netgraph.get_sugiyama_layout, and rustworkx.visualization.graphviz_draw... why pile on?

All of the above either depend on either the dot program from graphviz, or the pure-python Sugiyama implementation from grandalf.

The dot program is probably the best solution to this problem that currently exists, and you should use the dot program if you are working in an environment that allows you to install graphviz and your python application only needs to render very small graphs and/or you don't need to produce them quickly.

The grandalf-based solutions are pure-python which is awesome for portability, but is slow for medium/large graphs (because python). In addition, that project hasn't had maintenance in many years, including incorporating the 2020 Erratum from Brandes et., al on which the implementation appears to be based. This implementation suffers from some occasions layouts where two nodes can share position, which either reveals a bug referenced in the erratum, or in the original implementation. Anyway, the grandalf project has an excellent framework for this problem, and has a deep bench of potential mentors and prior contributors, so if you need a pure-python implementation you should support them.

This project is about having zero 3rd-party dependencies, reasonable multi-platform support (arm & x86 wheels are available for linux, windows, and mac), and speed. In service of all three of these goals, this library is a thin wrapper around the excellent rust crate rust-sugiyama, which is likely the clearest-eyed implementation of the complex Sugiyama algorithm that's available for free on the internet.

With these shoulders to stand on, this project aims to offer very good dot-like layouts, very quickly, and with no 3rd-party runtime dependencies. This is a great fit for folks who can't (or don't want to) install graphviz, or for web-applications that want smaller images, or for anyone needing a large directed graph layout to run fast.

Quick Start

import networkx as nx
from fast_sugiyama import from_edges

g = nx.gn_graph(42, seed=132, create_using=nx.DiGraph)
pos = from_edges(g.edges()).to_dict()
nx.draw_networkx(g, pos=pos, with_labels=False, node_size=150)
Quick Start Output

The above example is for a networkx graph, but the fast-sugiyama package can be used with any graph library. The return type from the to_dict() method works equally well for the rustworkx Pos2DMapping type as the networkx pos arg. It's a {node_id: (x, y)} coordinate mapping you can take where you please.

You might also wonder why we don't emulate the networkx layout api conventions exactly (e.g., immediately return exactly one layout), and we'll talk about that a little later.

Install

This package is pip installable.

pip install fast-sugiyama

As with all python endeavours, you should probably be using a virtual environment for your project via conda \ mamba or venv (preferrably via uv).

uv pip install fast-sugiyama

Either of the above two install commands should work well with either ARM or x86 systems running Linux, Mac, or Windows. If you have another system and want to run this package and you can't install/build/run from the sdist or the repo please open a detailed issue and let me know what happened. Maybe I can help.

Usage

Purely for simplicity, this section will use networkx to facilitate usage demonstration and discussion. Again, this project has no dependencies, and can be used to support high-quality layout visuals for any graph library that uses a node -> coord mapping.

The from_edges function returns a list of layouts, one for each weakly connected component of the input graph. Each layout is a tuple containing the positions, width, height, and edges returned by the solver. This makes it easy to deal with multi-graphs, and let's us write helper functions to support various ways of displaying separate graphs in one layout.

This section will use a multi-graph from the test suite that includes 40 component graphs, each with a range of sizes. In these examples, the full graph g is made of 40 components, 435 nodes, and 395 edges. There is also a sub-set of these components called sg that includes just 12 of the 40 components.

dot-like layouts

This package has a helper function for users who would like to get a layout (or a multi-graph layout) similar to the dot program -- specifically, similar to the layout produced by nx.pydot.pydot_layout. This layout aligns hierarchies to the top of the figure, lines them up side by side horizontally, and packs them efficiently such that nodes in neighboring graph components are separated by the same spacing interval as nodes within each component. The following plot compares the layout results for a 12 component multi-graph sg.

Pydot Compare

Note that the bounding boxes of the layouts are allowed to overlap in the pydot layout, so this library supports that as well.

rect-like / compact layouts

Lining up components horizontally quickly becomes unwieldy with the pydot layout though. Here's all 40 components of our multi-graph g.

Difficult Pydot Layout

Users can install an optional dependency rectangle-packer to produce an improved layout that compactly packs the components.

pos = from_edges(g.edges()).rect_pack_layouts(max_width=2500).to_dict()
Rect Pack Layout

One can even utilize the 'horizontal compact' idea from the dot-like layouts to tighten things up even further.

pos = from_edges(g.edges()).compact_layout(max_width=2500).to_dict()
# or equivalently
pos = (
    from_edges(g.edges())
    .rect_pack_layouts(max_width=2500)
    .sort_horizontal()
    .compact_layouts_horizontal()
)
Compact Layout

DIY layouts

The helper layouts above simply chain a couple layout 'primitive' operations. For example, the .dot_layout() is equivalent to .align_layouts_vertical_top().compact_layouts_horizontal() You can chain together the built in primitives, or adapt their code into your own custom functions to obtain any arrangement.

pos = (
    from_edges(g.edges())
    [:6] # chaining even works after slices
    .align_layouts_vertical()
    .align_layouts_horizontal_center()
    .to_dict()
)
Sliced, centered, vertically stacked

If you're feeling wild and your graph is not simple and acyclic, you can even capture the dummy vertices and their edges from the Sugiyama solver and render those.

Here's another graph from the test suite that is directed but is not a DAG. Let's say these edges are in a variable called edges.

g = nx.DiGraph()
g.add_edges_from(edges)
pos = from_edges(edges).to_dict()

fig, ax = plt.subplots()
nx.draw_networkx(g, pos=pos, ax=ax)
Hero with no dummies

But there's an issue with the edge from (15, 1) -- the algorithm created a gap for the edge to go around node 8, not behind it. We can render that edge path easily by using the edges returned by the solver in our layout.

layouts = from_edges(edges)
pos = layouts.to_dict()
nodes = list({n for edge in edges for n in edge})

g = nx.DiGraph()
for *_, edgelist in layouts: # the solver returns the edges with the dummy's included
    if edgelist is not None:
        g.add_edges_from(edgelist)

fig, ax = plt.subplots()

nx.draw_networkx_nodes(g, pos, ax=ax, nodelist=nodes)
nx.draw_networkx_labels(g, pos, ax=ax, labels={n: n for n in nodes})

_ = nx.draw_networkx_edges(
    g,
    pos,
    [e for e in g.edges() if e[1] not in nodes],
    arrows=False,
    ax=ax,
)
_ = nx.draw_networkx_edges(
    g,
    pos,
    [e for e in g.edges() if e[1] in nodes],
    node_size=[300 if n in nodes else 0 for n in g.nodes()],
    ax=ax,
)
Hero with dummies

Benchmarks

Speed is not the purpose of this package, but it is a welcome benefit of using rust for all the heavy lifting. There are vanishingly few non-pathological reasons to produce a hierarchical graph plot with 5,000+ nodes, but for reasonable graph sizes, say, under 1000, this package is ~100-400x faster than using pydot. NOTE: pydot will typically yield a nicer looking layout, and if you can install it in your environment and wait for pydot to do its serialization/deserialization, you should.

for n in [100, 500, 1000, 5000, 10000]:
    g = nx.gn_graph(n, seed=42)

    %timeit _ = from_edges(g.edges()).dot_layout().to_dict()
    %timeit _ = nx.nx_pydot.pydot_layout(g, prog='dot')
786 μs ± 6.88 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) <-- ours @ n==100 (410x faster)
325 ms ± 9.01 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
10.9 ms ± 112 μs per loop (mean ± std. dev. of 7 runs, 100 loops each) <-- ours @ n==500 (140x faster)
1.57 s ± 27.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
35.8 ms ± 48.2 μs per loop (mean ± std. dev. of 7 runs, 10 loops each) <-- ours @ n==1000 (89x faster)
3.2 s ± 8.41 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
855 ms ± 5.84 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) <-- ours @ n==5000 (21x faster)
18.7 s ± 234 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
3.1 s ± 23.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) <-- ours @ n==10000 (16x faster)
49.6 s ± 1.07 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

And if we plot this up...

Single Graph Benchmark

Rust Sugiyama

This project contains a fork of the excellent rust-sugiyama crate, and includes minor api changes to support layouts via the python bindings. This fork is not intended to ever be published to crates.io. Instead anything useful we do here will be offered to the upstream crate for consideration.

Differences from the upstream project currently include:

  • Compute layout width and height in layout units rather than node count
  • Optionally validate the layout to prevent/detect overlapping nodes & invalid layouts
  • Return dummy vertices and edges if they're part of the layout
  • Remove dev-dependencies that point to personal github repos and replace with alternatives

Big thank you to the developers of the rust-sugiyama crate for doing the heavy lifting!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fast_sugiyama-0.5.3.tar.gz (798.5 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

fast_sugiyama-0.5.3-cp311-abi3-win_arm64.whl (219.2 kB view details)

Uploaded CPython 3.11+Windows ARM64

fast_sugiyama-0.5.3-cp311-abi3-win_amd64.whl (231.4 kB view details)

Uploaded CPython 3.11+Windows x86-64

fast_sugiyama-0.5.3-cp311-abi3-manylinux_2_34_x86_64.whl (328.1 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.34+ x86-64

fast_sugiyama-0.5.3-cp311-abi3-manylinux_2_34_aarch64.whl (316.4 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.34+ ARM64

fast_sugiyama-0.5.3-cp311-abi3-macosx_11_0_arm64.whl (282.7 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

fast_sugiyama-0.5.3-cp311-abi3-macosx_10_12_x86_64.whl (310.1 kB view details)

Uploaded CPython 3.11+macOS 10.12+ x86-64

File details

Details for the file fast_sugiyama-0.5.3.tar.gz.

File metadata

  • Download URL: fast_sugiyama-0.5.3.tar.gz
  • Upload date:
  • Size: 798.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for fast_sugiyama-0.5.3.tar.gz
Algorithm Hash digest
SHA256 073cba7184418ac2df1dba2f8835ffc10a687db1f130bfa0a1228ab4e6827c4f
MD5 32a77214a1cb0c4b1b4ab62a85cab60d
BLAKE2b-256 8db34d23c2a3ac51da42de40e1a59c1a801963274665a98c24503e18ba5e28e4

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.3.tar.gz:

Publisher: python_release.yml on austinorr/fast-sugiyama

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fast_sugiyama-0.5.3-cp311-abi3-win_arm64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.3-cp311-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 ef4500892c400adb6471f33b31bec7fb107877d3bb60b178c2f73b125b2c0449
MD5 c24e3062f9324ad784bef769a960220b
BLAKE2b-256 1f644dc1b2a0f2185552f3be186683ca23e1d69a7fb0e730092ca163d071a5cf

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.3-cp311-abi3-win_arm64.whl:

Publisher: python_release.yml on austinorr/fast-sugiyama

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fast_sugiyama-0.5.3-cp311-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.3-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 a222fecca64a9db6b60dcf02fb9b3b8d69b9e9e7f592f70e830965899d612d78
MD5 7aeb5c44f56fc8373faf2fae0d984046
BLAKE2b-256 4ab998c6cad41de1023055c5d4bae7191ef258b192a36a1d3497bc9a12777abb

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.3-cp311-abi3-win_amd64.whl:

Publisher: python_release.yml on austinorr/fast-sugiyama

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fast_sugiyama-0.5.3-cp311-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.3-cp311-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 79ae9abd9a22398c87b161785c0f3174dfff25bf3d3464b45881a559860c6560
MD5 09dfeacf0bfd7a14c374af6addc9559e
BLAKE2b-256 0d11b4c3dfc25ffa9518c9148be9d1f7f1edb92c83fae46c4900249d3798c9cb

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.3-cp311-abi3-manylinux_2_34_x86_64.whl:

Publisher: python_release.yml on austinorr/fast-sugiyama

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fast_sugiyama-0.5.3-cp311-abi3-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.3-cp311-abi3-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 10b8112a05650feb84b82fbd46a2e5553986a3f5c122df52b2811d1a7b140143
MD5 cae9b6e3283e8e4afbd3122fbe3567e6
BLAKE2b-256 f2d4a93c2249e62eb01c8059b4dcd59fa0c2594cb0f642b55b16dbdf83147afa

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.3-cp311-abi3-manylinux_2_34_aarch64.whl:

Publisher: python_release.yml on austinorr/fast-sugiyama

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fast_sugiyama-0.5.3-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.3-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a93e94bbf815ec7255a1e2515e65796f32bcd1fdff9ec232b006e0c1c50e970d
MD5 37a01bedd194dcd3a0b24eddeb11ce11
BLAKE2b-256 339d55ac5653b95961b92a1a2aa78871a0a65956b1fdb1f6019a8bf020a8fcef

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.3-cp311-abi3-macosx_11_0_arm64.whl:

Publisher: python_release.yml on austinorr/fast-sugiyama

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fast_sugiyama-0.5.3-cp311-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.3-cp311-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 bb08aae01fc62fd6a7ad7a67387d085bd4016c538c05d5a80b6264cb89e43890
MD5 bd94aa01d43574d3c54ddb742ff79d12
BLAKE2b-256 9fe119bf4e9600edebd1b9d62a4e8413327a2e11f08040369e14ce5db05cefa2

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.3-cp311-abi3-macosx_10_12_x86_64.whl:

Publisher: python_release.yml on austinorr/fast-sugiyama

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page