Inspired by XKCD #2347, Stacktower renders dependency graphs as physical towers where blocks rest on what they depend on. Your application sits at the top, supported by libraries below—all the way down to that one critical package maintained by some dude in Nebraska.
📖 Read the full story at stacktower.io
⚠️ Note: The ordering algorithms are still experimental and may not produce nicely stacked towers for projects with a large number of dependencies. Results can vary. We're actively working on improvements.
brew install matzehuels/tap/stacktowergo install github.com/matzehuels/stacktower@latestgit clone https://github.com/matzehuels/stacktower.git
cd stacktower
go build -o stacktower .# Render the included Flask example (XKCD-style tower is the default)
stacktower render examples/real/flask.json -o flask.svgStacktower works in two stages: parse dependency data from package registries or manifest files, then render visualizations.
The parse command auto-detects whether you're providing a package name or a manifest file:
stacktower parse <language> <package-or-file> [flags]Supported languages: python, rust, javascript, ruby, php, java, go
stacktower parse python fastapi -o fastapi.json # PyPI
stacktower parse rust serde -o serde.json # crates.io
stacktower parse javascript yargs -o yargs.json # npm
stacktower parse ruby rails -o rails.json # RubyGems
stacktower parse php monolog/monolog -o monolog.json # Packagist
stacktower parse java com.google.guava:guava -o guava.json # Maven Central
stacktower parse go github.com/gin-gonic/gin -o gin.json # Go Module Proxystacktower parse python examples/manifest/poetry.lock -o deps.json
stacktower parse python examples/manifest/requirements.txt -o deps.json
stacktower parse rust examples/manifest/Cargo.toml -o deps.json
stacktower parse javascript examples/manifest/package.json -o deps.json
stacktower parse ruby examples/manifest/Gemfile -o deps.json
stacktower parse php examples/manifest/composer.json -o deps.json
stacktower parse java examples/manifest/pom.xml -o deps.json
stacktower parse go examples/manifest/go.mod -o deps.jsonWhen the argument exists on disk or matches a known manifest filename, Stacktower automatically parses it as a manifest.
The project name (root node) is auto-detected from the manifest or a sibling file:
- Cargo.toml:
[package].name - go.mod:
moduledirective - package.json:
namefield - composer.json:
namefield - pom.xml:
groupId:artifactId - poetry.lock / requirements.txt:
pyproject.toml(sibling) - Gemfile:
*.gemspec(sibling)
Use --name to override the auto-detected name:
stacktower parse python requirements.txt --name="my-project" -o deps.json
stacktower parse ruby Gemfile -n my-rails-app -o deps.jsonForce registry or manifest parsing when auto-detection isn't enough:
# Force registry lookup
stacktower parse python registry pypi fastapi
stacktower parse java registry maven org.springframework:spring-core
stacktower parse go registry goproxy github.com/spf13/cobra
# Force manifest type
stacktower parse python manifest poetry examples/manifest/poetry.lock
stacktower parse python manifest requirements examples/manifest/requirements.txt
stacktower parse rust manifest cargo examples/manifest/Cargo.toml
stacktower parse javascript manifest package examples/manifest/package.json
stacktower parse ruby manifest gemfile examples/manifest/Gemfile
stacktower parse php manifest composer examples/manifest/composer.json
stacktower parse java manifest pom examples/manifest/pom.xml
stacktower parse go manifest gomod examples/manifest/go.modBy default, Stacktower enriches packages with GitHub metadata (stars, maintainers, last commit) for richer visualizations. Set GITHUB_TOKEN to enable this:
export GITHUB_TOKEN=your_token
stacktower parse python fastapi -o fastapi.json
# Disable enrichment if you don't have a token
stacktower parse python fastapi --enrich=false -o fastapi.jsonThe render command generates visualizations from parsed JSON graphs:
stacktower render <file> [flags]# Hand-drawn XKCD-style tower (default)
stacktower render examples/real/flask.json -o flask.svg
# Disable hand-drawn effects for a cleaner look
stacktower render examples/real/serde.json --style simple --randomize=false --popups=false -o serde.svg
# Traditional node-link diagram (uses Graphviz DOT)
stacktower render examples/real/yargs.json -t nodelink -o yargs.svg
# Multiple types at once (outputs flask_tower.svg, flask_nodelink.svg)
stacktower render examples/real/flask.json -t tower,nodelink -o flask# SVG output (default)
stacktower render examples/real/flask.json -o flask.svg
# JSON layout export (for external tools or re-rendering)
stacktower render examples/real/flask.json -f json -o flask.json
# PDF output
stacktower render examples/real/flask.json -f pdf -o flask.pdf
# PNG output (2x scale by default)
stacktower render examples/real/flask.json -f png -o flask.png
# Multiple formats at once (outputs flask.svg, flask.json, flask.pdf)
stacktower render examples/real/flask.json -f svg,json,pdf -o flask
# Combine multiple types and formats
stacktower render examples/real/flask.json -t tower,nodelink -f svg,jsonOutput path behavior:
- No
-o: Derives from input (input.json→input.<format>) - Single format: Uses exact path (
-o out.svg→out.svg) - Multiple formats: Strips extension, adds format (
-o out.svg -f svg,json→out.svg,out.json) - Multiple types: Adds type suffix (
-t tower,nodelink→out_tower.svg,out_nodelink.svg)
Note: PDF and PNG output requires librsvg:
- macOS:
brew install librsvg- Linux:
apt install librsvg2-bin
The repository ships with pre-parsed graphs so you can experiment immediately:
# Real packages with full metadata (XKCD-style by default)
stacktower render examples/real/flask.json -o flask.svg
stacktower render examples/real/serde.json -o serde.svg
stacktower render examples/real/yargs.json -o yargs.svg
# With Nebraska guy ranking
stacktower render examples/real/flask.json --nebraska -o flask.svg
# Synthetic test cases
stacktower render examples/test/diamond.json -o diamond.svg| Flag | Description |
|---|---|
-v, --verbose |
Enable debug logging (search space info, timing details) |
| Flag | Description |
|---|---|
-o, --output |
Output file (stdout if empty) |
-n, --name |
Project name for manifest parsing (auto-detected from manifest if not set) |
--max-depth N |
Maximum dependency depth (default: 10) |
--max-nodes N |
Maximum packages to fetch (default: 5000) |
--enrich |
Enrich with GitHub metadata (default: true, requires GITHUB_TOKEN) |
--refresh |
Bypass cache |
| Flag | Description |
|---|---|
-o, --output |
Output file or base path for multiple types/formats |
-t, --type |
Visualization type(s): tower (default), nodelink (comma-separated) |
-f, --format |
Output format(s): svg (default), json, pdf, png (comma-separated) |
--normalize |
Apply graph normalization: break cycles, remove transitive edges, assign layers, subdivide long edges (default: true) |
| Flag | Description |
|---|---|
--width N |
Frame width in pixels (default: 800) |
--height N |
Frame height in pixels (default: 600) |
--style handdrawn|simple |
Visual style (default: handdrawn) |
--randomize |
Vary block widths to visualize load-bearing structure (default: true) |
--merge |
Merge subdivider blocks into continuous towers (default: true) |
--popups |
Enable hover popups with package metadata (default: true) |
--nebraska |
Show "Nebraska guy" maintainer ranking panel |
--edges |
Show dependency edges as dashed lines |
--ordering optimal|barycentric |
Crossing minimization algorithm (default: optimal) |
--ordering-timeout N |
Timeout for optimal search in seconds (default: 60) |
--top-down |
Width flows from roots down; by default width flows from sinks up |
| Flag | Description |
|---|---|
--detailed |
Show all node metadata in labels |
The render layer accepts a simple JSON format, making it easy to visualize any directed graph—not just package dependencies. You can hand-craft graphs for component diagrams, callgraphs, or pipe output from other tools.
{
"nodes": [
{ "id": "app" },
{ "id": "lib-a" },
{ "id": "lib-b" }
],
"edges": [
{ "from": "app", "to": "lib-a" },
{ "from": "lib-a", "to": "lib-b" }
]
}| Field | Type | Description |
|---|---|---|
nodes[].id |
string | Unique node identifier (displayed as label) |
edges[].from |
string | Source node ID |
edges[].to |
string | Target node ID |
| Field | Type | Description |
|---|---|---|
nodes[].row |
int | Pre-assigned layer (computed automatically if omitted) |
nodes[].kind |
string | Internal use: "subdivider" or "auxiliary" |
nodes[].meta |
object | Freeform metadata for display features |
These keys are read by specific render flags. All are optional—missing keys simply disable the corresponding feature.
| Key | Type | Used By |
|---|---|---|
repo_url |
string | Clickable blocks, --popups, --nebraska |
repo_stars |
int | --popups |
repo_owner |
string | --nebraska |
repo_maintainers |
[]string | --nebraska |
repo_last_commit |
string (date) | --popups, brittle detection |
repo_last_release |
string (date) | --popups |
repo_archived |
bool | --popups, brittle detection |
summary |
string | --popups (fallback: description) |
The --detailed flag (node-link only) displays all meta keys in the node label.
- Parse — Fetch package metadata from registries or local manifest files
- Reduce — Remove transitive edges to show only direct dependencies
- Layer — Assign each package to a row based on its depth
- Order — Minimize edge crossings using branch-and-bound with PQ-tree pruning
- Layout — Compute block widths proportional to downstream dependents
- Render — Generate output in SVG, JSON, PDF, or PNG format
The ordering step is where the magic happens. Stacktower uses an optimal search algorithm that guarantees minimum crossings for small-to-medium graphs. For larger graphs, it gracefully falls back after a configurable timeout.
| Variable | Description |
|---|---|
GITHUB_TOKEN |
GitHub API token for --enrich metadata |
GITLAB_TOKEN |
GitLab API token for --enrich metadata |
HTTP responses are cached in ~/.cache/stacktower/ with a 24-hour TTL. Use --refresh to bypass the cache for a single request.
# Clear the entire cache
stacktower cache clear
# Show cache directory path
stacktower cache pathStacktower can be used as a Go library for programmatic graph visualization.
import (
"github.com/matzehuels/stacktower/pkg/dag"
"github.com/matzehuels/stacktower/pkg/dag/transform"
"github.com/matzehuels/stacktower/pkg/render/tower/layout"
"github.com/matzehuels/stacktower/pkg/render/tower/sink"
)
// Build a graph
g := dag.New(nil)
g.AddNode(dag.Node{ID: "app", Row: 0})
g.AddNode(dag.Node{ID: "lib", Row: 1})
g.AddEdge(dag.Edge{From: "app", To: "lib"})
// Normalize and render
transform.Normalize(g)
l := layout.Build(g, 800, 600)
svg := sink.RenderSVG(l, sink.WithGraph(g), sink.WithPopups())📚 Full API documentation on pkg.go.dev
Key packages:
pkg/dag— DAG data structure and crossing algorithmspkg/dag/transform— Graph normalization pipelinepkg/render/tower— Layout, ordering, and renderingpkg/deps— Dependency resolution from registries
See CONTRIBUTING.md for guidelines on adding new languages, manifest parsers, or output formats.
make install-tools # Install required tools (golangci-lint, goimports, govulncheck)
make check # Run all CI checks locally (fmt, lint, test, vuln)
make build # Build binary to bin/stacktower| Command | Description |
|---|---|
make check |
Format, lint, test, vulncheck (same as CI) |
make fmt |
Format code with gofmt and goimports |
make lint |
Run golangci-lint |
make test |
Run tests with race detector |
make cover |
Run tests with coverage report |
make vuln |
Check for known vulnerabilities |
make e2e |
Run end-to-end tests |
make snapshot |
Build release locally (no publish) |
Commit messages follow Conventional Commits.
- 📖 stacktower.io — Interactive examples and the full story behind tower visualizations
- 🐛 Issues — Bug reports and feature requests
Apache-2.0