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.
🚀 Try the Web App · 📖 Read the Story
⚠️ 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 bin/stacktower ./cmd/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.jsonBy 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
# Skip enrichment if you don't have a token
stacktower parse python fastapi --skip-enrich -o fastapi.jsonThe render command generates visualizations from parsed JSON graphs:
stacktower render <file> [flags]This is a shortcut that combines layout and visualize in one step. For more control, you can run them separately:
# Two-step workflow with intermediate layout
stacktower layout examples/real/flask.json -o flask.layout.json
stacktower visualize flask.layout.json -o flask.svg# 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# 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 flaskOutput 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 -f svg,json→out.svg,out.json)
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) |
--skip-enrich |
Skip metadata enrichment (GitHub descriptions, etc.) |
--no-cache |
Disable caching |
| Flag | Description |
|---|---|
-o, --output |
Output file or base path for multiple formats |
-t, --type |
Visualization type: tower (default), nodelink |
-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) |
--no-cache |
Disable caching |
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.
For parsing repositories directly from GitHub, you can authenticate using the device flow:
# Login with GitHub (opens browser for device authorization)
stacktower github login
# Check current session
stacktower github whoami
# Logout
stacktower github logout
# Parse a manifest from a GitHub repository
stacktower parse github owner/repo -o deps.json| Variable | Description |
|---|---|
GITHUB_TOKEN |
GitHub API token for metadata enrichment |
GITLAB_TOKEN |
GitLab API token for metadata enrichment |
HTTP responses are cached in ~/.cache/stacktower/ with a 24-hour TTL. Use --no-cache to disable caching 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/core/dag"
"github.com/matzehuels/stacktower/pkg/core/dag/transform"
"github.com/matzehuels/stacktower/pkg/core/render/tower/layout"
"github.com/matzehuels/stacktower/pkg/core/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/core/dag— DAG data structure and crossing algorithmspkg/core/dag/transform— Graph normalization pipelinepkg/core/render/tower— Layout, ordering, and renderingpkg/core/deps— Dependency resolution from registriespkg/pipeline— Complete parse → layout → render pipeline
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