Skip to main content

A fast and flexible hierarchical graph layout engine.

Project description

Fast-Sugiyama

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.1.tar.gz (787.8 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.1-cp311-abi3-win_arm64.whl (159.2 kB view details)

Uploaded CPython 3.11+Windows ARM64

fast_sugiyama-0.5.1-cp311-abi3-win_amd64.whl (174.8 kB view details)

Uploaded CPython 3.11+Windows x86-64

fast_sugiyama-0.5.1-cp311-abi3-manylinux_2_34_x86_64.whl (171.7 kB view details)

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

fast_sugiyama-0.5.1-cp311-abi3-manylinux_2_34_aarch64.whl (156.8 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.34+ ARM64

fast_sugiyama-0.5.1-cp311-abi3-macosx_11_0_arm64.whl (151.0 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

fast_sugiyama-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl (167.3 kB view details)

Uploaded CPython 3.11+macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: fast_sugiyama-0.5.1.tar.gz
  • Upload date:
  • Size: 787.8 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.1.tar.gz
Algorithm Hash digest
SHA256 a31cdef528b49aecb40161f5685be9b2b93242dc653994b022dd6203f846fd6f
MD5 534419b8ced23a56cfa6eb84b87eb0c7
BLAKE2b-256 4e3a347d7fe901ccff0ffc06145b8dbd22ada08713368f5d7a76a3c6325a4404

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.1.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.1-cp311-abi3-win_arm64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.1-cp311-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 3ab6cc78ebcca3717f00f2f74b3971e495b100231e1a8ff72e0df2cabacbad01
MD5 4c80cc23763d56cfad1e2cf43a022980
BLAKE2b-256 187d20b2008c9122dacd7a1aa91dd9d6b6337ec957678c8e73ad8ad30d84d9c7

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.1-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.1-cp311-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.1-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 d3c7c8d26b1c0524ff511fd30d30cb3b1a6cbbdce943f900bfcb2ef9160ed1d9
MD5 486fe69997d19409f478a76074d83c08
BLAKE2b-256 57e56b41c2f567aee61ffd42786ca23a72b4c6b1ebc40ff70d6479c69777d827

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.1-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.1-cp311-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.1-cp311-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 9d1cccf319ce3aab88a187ac89535bc1f9195a0b2926de2ec4f28a970b3e3cba
MD5 030e8f5e598777f6a4f07c9e58b05ba2
BLAKE2b-256 a16b0ac1fcee1d15be9bb15d9a6aa39ca53eb9b3ef52a18f7c88f8121fec916f

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.1-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.1-cp311-abi3-manylinux_2_34_aarch64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.1-cp311-abi3-manylinux_2_34_aarch64.whl
Algorithm Hash digest
SHA256 7fb2f30749767cddd22832b0850d6a512e509d362e9187fce7dfeeea641656fe
MD5 59c5d7052785af503515a620ad059fd5
BLAKE2b-256 e9adc4a7d6b339421e7203ba55e969ed9409df2644556cd392972ace93b19175

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.1-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.1-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.1-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d6f971e180a1cdac5568bfc3d84763aca8ab7b3847608682d0571eaeceb2d891
MD5 a1643ed60e80a1d7f3512085c5a146e8
BLAKE2b-256 ba6948b72a86ff47e8624a077b8567f35b77289c89dbba638340d4310a57400e

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.1-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.1-cp311-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for fast_sugiyama-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 3ae4d9f5e7eb2f8674acc8dc22da6322abecc855aa9e09f703a3bb0f83f82531
MD5 e12c2aa6715bb723695b2ef1964ee2d7
BLAKE2b-256 364119b5d626f880a5347149c2d70954933bb6fdd77f573a55253dc4f82e5904

See more details on using hashes here.

Provenance

The following attestation bundles were made for fast_sugiyama-0.5.1-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