- Python 99.9%
- Shell 0.1%
| screenshots | ||
| LICENSE.txt | ||
| orgheadingnetwork.py | ||
| pyproject.toml | ||
| README.org | ||
| regenerate_notes.sh | ||
| uv.lock | ||
orgheadingnetwork
This tool parses one or more Orgdown files (= syntax of Emacs Org-mode), extracts headings with their metadata and inter-heading links, classifies the link types by strength, and generates an interactive HTML visualization of the resulting network.
You can also limit the visualization for a specific sub-hierarchy of an Orgdown file.
Link types from weakest to strongest:
- Parent/Child — implicit hierarchy from heading nesting
- Broken outgoing — ID reference to a heading not found in the input files
- Uni-directional — one heading links to another by ID
- Bi-directional — two headings link to each other by ID
Only the strongest link type between any two headings is kept.
Screenshots
Overview of the network visualization of my notes.org containing 14491 headings:
Selecting a node highlights its connections and shows details:
Using the link on the right hand side, you can navigate to linked headings and re-center the visualization there.
Searching for nodes shows a ranked dropdown list for quick navigation:
Search
The search box in the top-left panel finds nodes by title, tags, ID, or source file name. As you type, non-matching nodes are dimmed on the canvas and a dropdown list shows up to 50 results ranked by relevance:
- Exact title match (highest)
- Title starts with the query
- Exact tag match
- Title contains the query
- Tag contains the query
- ID or file name contains the query (lowest)
Click any result to center the view on that node. You can also navigate the list with Arrow Up/Down, press Enter to jump to the highlighted entry, or Escape to clear the search.
Recognized ID Sources
The tool extracts IDs from several places within each heading. The following example demonstrates all of them:
* TODO Example heading with [[id:aaa-title-link][a link]] in the title
:PROPERTIES:
:ID: 1234-abcd-5678-efgh
:TRIGGER: ids("id:bbb-trigger-id1" "ccc-trigger-id2")
:BLOCKER: ids(ddd-blocker-id)
:END:
This body text contains a link [[id:eee-body-link][to another heading]]
and also a bare link id:fff-body-bare without description.
Even a plain id:ggg-plain-ref in running text is recognized, as well as
an ID hidden in a link description: [[https://example.com][see id:hhh-in-desc]].
IDs extracted from this heading:
| Source | Extracted ID | How |
|---|---|---|
| Title | aaa-title-link |
[[id:...][desc]] link in heading title |
:ID: property |
1234-abcd-5678-efgh |
Becomes this heading's own ID |
:TRIGGER: |
bbb-trigger-id1 |
org-edna ids("id:...") syntax |
:TRIGGER: |
ccc-trigger-id2 |
org-edna ids("...") syntax (no prefix) |
:BLOCKER: |
ddd-blocker-id |
org-edna ids(...) unquoted syntax |
| Body | eee-body-link |
[[id:...][desc]] link in body |
| Body | fff-body-bare |
[[id:...]] link without description |
| Body | ggg-plain-ref |
Bare id:... reference in text |
| Body | hhh-in-desc |
id:... inside a link description |
Headings without an :ID: property receive an auto-generated UUID
(prefixed temp-) so they can still participate in the hierarchy.
Usage
Requires Python 3.13+ and uv.
# Install dependencies
uv sync
# Basic: parse org files, produce org-network.html
uv run orgheadingnetwork.py file1.org file2.org
# Custom output path
uv run orgheadingnetwork.py -o my-network.html file.org
# Visualize only a sub-hierarchy (see below)
uv run orgheadingnetwork.py --subtree 2026-02-my-heading-id file.org
# Open result in default browser
uv run orgheadingnetwork.py --open file.org
# Dump the parsed data structure to stdout (for debugging)
uv run orgheadingnetwork.py --dump file.org
# Verbose logging
uv run orgheadingnetwork.py -v file.org
# Quiet (errors only)
uv run orgheadingnetwork.py -q file.org
The output HTML file is self-contained (loads D3.js from CDN) and can be opened directly in a browser. It provides an interactive force-directed graph with zoom, pan, search, link-type filtering, and click-to-inspect.
Note: Broken outgoing links are hidden by default in the visualization. Use the "Broken" checkbox in the link type filter to show them.
You can use an Elisp function to invoke this tool as well. I wrote
my-orgheadingnetwork-current-file() which you can get from my Emacs
configuration. You'll need to adapt it to your paths and so forth.
With that function, it's really convenient to get a visualization - in
my case for the current Org-mode file.
There's also an Elisp function for visualizing the current sub-hierarchy:
my-orgheadingnetwork-current-heading()
Subtree Mode
Use --subtree ID to visualize only the sub-hierarchy rooted at a
specific heading. This requires exactly one input file.
uv run orgheadingnetwork.py --subtree 2026-02-my-heading-id file.org
The ID parameter must match the :ID: property value of a heading
in the file. An optional id: prefix is accepted and stripped, so
--subtree foo-bar and --subtree id:foo-bar are equivalent:
* My project heading :PROPERTIES: :ID: 2026-02-my-heading-id :END:
The visualization will include:
- The heading matching the given ID (as the root of the sub-graph)
- All its descendants (children, grandchildren, etc.)
Links from subtree headings to headings outside the subtree (but still in the same file) are treated as broken outgoing links. Uni-directional incoming links from outside the subtree are ignored. Bi-directional links where one end is inside the subtree are kept as broken outgoing links from the subtree side.
The HTML title shows Subtree "[heading title]" of "[filename]" so
you can tell which mode produced the output.
Combining --subtree with multiple input files is not allowed and
produces an error.
Author
Karl Voit, https://Karl-Voit.at/


