Skip to content

feat: openkb visualize — interactive 3D knowledge graph#103

Open
KylinMountain wants to merge 13 commits into
mainfrom
feat/visualize
Open

feat: openkb visualize — interactive 3D knowledge graph#103
KylinMountain wants to merge 13 commits into
mainfrom
feat/visualize

Conversation

@KylinMountain

Copy link
Copy Markdown
Collaborator

Summary

  • New openkb visualize command — renders the wiki's [[wikilink]] graph as an interactive 3D knowledge graph: self-contained offline HTML, no LLM, pure data + template.
  • 3D orbit: nodes laid out on a sphere with perspective depth + fog; drag the background to rotate, scroll to zoom (cursor-anchored — dive into a cluster), drag a node to pull it out & pin it (double-click to release).
  • Inspect & focus: click a node for a glass side-panel (type chip, description, sources, in/out links); hovering — or an open panel — spotlights that node's connections; legend filters by type; search centers a node.
  • Look: neon-on-dark "aurora" aesthetic, degree-scaled glowing nodes, flow-particle highlighted edges; dense-graph physics uses degree-normalized springs + simulated annealing so the layout settles and stops instead of jittering.
  • Opens in the browser by default (--no-open for headless); writes output/visualize/graph.html each run (latest snapshot, shareable).

Architecture: openkb/visualize.py (build_graph + render_html) · openkb/templates/graph.html (self-contained canvas template) · a thin visualize CLI command. Reuses lint wikilink extraction + frontmatter.parse; the template ships via hatchling's default packaging.

Also folds in a one-line fix to a deck-skill test that was already red on main (unrelated to visualize — #101 changed the default deck skill without updating the test).

Test plan

  • pytest — 810 passed
  • ruff clean on new files
  • Verified in-browser on a real KB (71 nodes / 800 edges): rotate, cursor-anchored zoom, hover spotlight + flow particles, click→panel, legend filter, drag-pin, settle-and-freeze, default auto-open

…enkb-deck-neon

#101 made openkb-deck-neon the default deck skill (creator.py
DEFAULT_DECK_SKILL) but left this test asserting the old
openkb-deck-editorial, so it has been red on main. Unrelated to the
visualize feature on this branch.
- canvas was a replaced element with width:auto, so inset:0 was ignored
  and it stayed at its intrinsic backing-store size — a doubling feedback
  loop (clientWidth←backing, backing←clientWidth*DPR) that never matched
  the window and rendered soft on retina. Add width:100%/height:100% so
  the CSS box tracks the viewport and the DPR backing store is correct.
- with 71 nodes / 800 edges every label drew at rest → unreadable pileup.
  Show labels only for the most-connected hubs at rest; reveal the rest on
  hover (node + neighbors), on selection, or when zoomed in (scale>1.4).
  Add a dark text shadow for legibility over the edge web.
A relative --kb-dir override (or relative default_kb config) left the
output path relative; Path.as_uri() raises on relative paths, so
`visualize --open` would crash after writing the HTML. resolve() first.
A dense KB (here 71 nodes / 800 edges, avg degree ~22) made the
force-directed layout swing violently and never settle — hubs were
pulled by ~22 springs at once, the summed force outran the damping, and
nodes flew around illegibly. Legend filtering looked broken only because
the motion masked it. Fixes:
- degree-normalize spring pull (1/min(deg_a,deg_b)) so a hub doesn't feel
  N x the force
- floor the repulsion distance so near-coincident nodes don't explode
- cap per-frame speed (VMAX) and damp harder (0.9 -> 0.84)
- simulated annealing: an alpha temperature cools to ~0 so physics freezes
  and the graph holds still; dragging a node or toggling a legend type
  reheats it to re-settle.

Verified in-browser on the real KB: settles in ~1.5s, fully still by ~5s,
and legend filtering now visibly re-packs the remaining nodes.
Upgrade the force-directed graph from 2D to 3D so it can be rotated:
- nodes now carry a z coordinate; physics (repulsion, degree-normalized
  springs, origin pull, annealing, speed cap) all run in 3D
- seed on a golden-angle sphere so 3D structure reads immediately
- per-frame project(): yaw/pitch rotation + perspective projection, with
  near=larger discs and depth fog (far nodes dim/recede)
- draw nodes far→near (painter's algorithm) so nearer ones occlude
- drag the background to orbit (yaw/pitch); scroll still zooms; dragging a
  node moves it in the current view plane via unproject(); pick() is now
  screen-space so hover/click stay accurate at any angle
- hover highlight + flow particles, click→glass panel, legend filter all
  preserved and verified in-browser on the real KB.

Default zoom bumped (scale 1.35) so the sphere fills more of the canvas.
Address feedback that hovering flashed the scene and that exploring was
hard:
- smooth, eased highlight (per-node hl lerp) replaces the hard faded
  snap; non-neighbors dim gently (never to black) and the hover pulse/
  glow are toned down — no more flicker
- an open inspector now keeps its node's connections lit (focus =
  hover || selected), so you can study one node's links at leisure
- orbit sensitivity halved (0.0045) — rotation is no longer twitchy
- zoom range widened to 9x and anchored on the cursor, so you can drill
  into a local cluster
- drag a node to pull it out; releasing pins it (dashed ring, won't
  spring back) so its links fan out for inspection; double-click releases
- hint line updated to match
Auto-open after generating so plain `openkb visualize` just shows the
graph; pass --no-open for headless/CI use. The HTML is still written to
output/visualize/graph.html every run (latest snapshot, shareable). If a
browser can't be launched, print a hint instead of failing.
- graph.html project(): clamp the perspective denominator (FOCAL+z2) so a
  node crossing the camera plane can't divide by ~0 → ±Infinity/NaN, which
  previously flung the node off-screen and (via centerOn) poisoned panX/panY
  until reload.
- graph.html: require >4px of motion before a press counts as a drag, so a
  normal click (with sub-pixel jitter) inspects the node instead of pinning it.
- visualize.py: reuse schema.PAGE_CONTENT_DIRS instead of a local _NODE_DIRS
  copy, and derive the fallback type so a new content dir can't KeyError.
- visualize.py: read each wiki file once (cache text for the edge pass)
  instead of twice.
- build_graph: summaries have no `sources` field — their origin is
  `full_text` (e.g. sources/nvda-10q.md). Include it so summary nodes show
  their source in the inspector instead of 'none'.
- graph.html cleanup from review: cache per-node color (drops a per-frame
  colorOf lookup), store adjacency as a Set (O(1) neighbour test, was
  O(deg) per node per frame), one degree-sort feeding both hubs and labels,
  a TAU constant for the seven full-circle arcs, and a corrected comment on
  the spring degree-softening (it intentionally only damps hub-hub edges).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant