A simple visualization of Orgdown (syntax of Emacs Org-mode) headings and their implicit (hierarchy) and explicit (links) interconnections
  • Python 99.9%
  • Shell 0.1%
Find a file
2026-03-01 18:26:08 +01:00
screenshots Add search dropdown with relevance-ranked results and screenshot 2026-02-15 15:13:22 +01:00
LICENSE.txt LICENSE.txt 2026-02-15 15:44:49 +01:00
orgheadingnetwork.py updated pyproject.toml and published 2026.3.1.1 as first version for pypi 2026-03-01 15:05:25 +01:00
pyproject.toml updated pyproject.toml and published 2026.3.1.1 as first version for pypi 2026-03-01 15:05:25 +01:00
README.org README.org: updated section on how to thank me 2026-03-01 18:26:08 +01:00
regenerate_notes.sh Rename orgheadingnetwork-claude.py to orgheadingnetwork.py 2026-02-15 15:15:05 +01:00
uv.lock Rename orgheadingnetwork-claude.py to orgheadingnetwork.py 2026-02-15 15:15:05 +01:00

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:

  1. Parent/Child — implicit hierarchy from heading nesting
  2. Broken outgoing — ID reference to a heading not found in the input files
  3. Uni-directional — one heading links to another by ID
  4. 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:

/publicvoit/orgheadingnetwork/media/branch/main/screenshots/2026-02-15T14.33.16%20orgheadingnetwork%20of%20notes.org%20-%20Overview%20--%20screenshots%20preview.png

Selecting a node highlights its connections and shows details:

/publicvoit/orgheadingnetwork/media/branch/main/screenshots/2026-02-15T14.34.52%20orgheadingnetwork%20of%20notes.org%20with%20Emacs%20node%20selected%20--%20screenshots%20preview.png

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:

/publicvoit/orgheadingnetwork/media/branch/main/screenshots/2026-02-15T15.07.51%20orgheadingnetwork%20with%20notes.org%20and%20search%20for%20Emacs%20--%20screenshots%20preview.png

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

How to Thank Me