Skip to content

Commit 0838dfd

Browse files
committed
fix(visualize): address code-review findings
- 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.
1 parent a2376b9 commit 0838dfd

2 files changed

Lines changed: 30 additions & 22 deletions

File tree

openkb/templates/graph.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@
291291
const z1 = -n.x*syw + n.z*cyw;
292292
const y2 = n.y*cpt - z1*spt; // then about X (pitch)
293293
const z2 = n.y*spt + z1*cpt;
294-
const pp = FOCAL/(FOCAL + z2); // perspective: near → larger
294+
const pp = FOCAL/Math.max(FOCAL + z2, FOCAL*0.25); // perspective: near→larger; clamp denom so a node crossing the camera plane can't blow up to ±Infinity/NaN
295295
n.rz = z2; n.pp = pp;
296296
n.sx = ox + x1*pp*scale;
297297
n.sy = oy + y2*pp*scale;
@@ -442,8 +442,11 @@
442442
/* ===================== hover / drag / pan ===================== */
443443
canvas.addEventListener("mousemove", e => {
444444
if(drag){
445+
// ignore sub-pixel jitter so a plain click still inspects instead of pinning
446+
if(!didDrag && Math.hypot(e.offsetX - oStartX, e.offsetY - oStartY) < 4) return;
447+
didDrag = true;
445448
const w = unproject(e.offsetX, e.offsetY, drag); // move the node within the current view plane
446-
drag.x = w.x; drag.y = w.y; drag.z = w.z; drag.vx = drag.vy = drag.vz = 0; didDrag = true;
449+
drag.x = w.x; drag.y = w.y; drag.z = w.z; drag.vx = drag.vy = drag.vz = 0;
447450
alpha = Math.max(alpha, 0.5); // reheat so neighbors re-settle around the dragged node
448451
return;
449452
}
@@ -458,8 +461,9 @@
458461
canvas.addEventListener("mousedown", e => {
459462
const n = pick(e.offsetX, e.offsetY);
460463
didDrag = false;
464+
oStartX = e.offsetX; oStartY = e.offsetY; // press origin — for the drag threshold and for orbit
461465
if(n){ drag = n; hover = n.id; canvas.style.cursor = "grabbing"; }
462-
else { orbiting = true; oStartX = e.offsetX; oStartY = e.offsetY; yawStart = yaw; pitchStart = pitch; canvas.style.cursor = "grabbing"; }
466+
else { orbiting = true; yawStart = yaw; pitchStart = pitch; canvas.style.cursor = "grabbing"; }
463467
});
464468
window.addEventListener("mouseup", e => {
465469
if(drag && !didDrag){ openPanel(drag); } // click (no drag) → inspect

openkb/visualize.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,32 @@
77

88
from openkb import frontmatter
99
from openkb.lint import _extract_wikilinks, _normalize_target
10+
from openkb.schema import PAGE_CONTENT_DIRS
1011

11-
_NODE_DIRS = ("summaries", "concepts", "entities")
12-
_FALLBACK_TYPE = {"summaries": "Summary", "concepts": "Concept", "entities": "Entity"}
12+
# Singular display type per content dir; falls back to a derived name for any
13+
# dir not listed (so a new PAGE_CONTENT_DIRS entry never KeyErrors here).
14+
_DIR_TYPE = {"summaries": "Summary", "concepts": "Concept", "entities": "Entity"}
15+
16+
17+
def _type_for_dir(sub: str) -> str:
18+
return _DIR_TYPE.get(sub) or sub.rstrip("s").capitalize() or sub
1319

1420

1521
def build_graph(wiki_dir: Path) -> dict:
1622
"""Collect nodes (pages), directed edges (wikilinks), and the set of types."""
1723
nodes: dict[str, dict] = {}
18-
for sub in _NODE_DIRS:
24+
texts: dict[str, str] = {} # nid -> file text, read once and reused for edges
25+
for sub in PAGE_CONTENT_DIRS:
1926
d = wiki_dir / sub
2027
if not d.exists():
2128
continue
2229
for p in sorted(d.glob("*.md")):
2330
nid = f"{sub}/{p.stem}"
24-
fm = frontmatter.parse(p.read_text(encoding="utf-8"))
31+
text = p.read_text(encoding="utf-8")
32+
texts[nid] = text
33+
fm = frontmatter.parse(text)
2534
t = fm.get("type")
26-
t = t.strip() if isinstance(t, str) and t.strip() else _FALLBACK_TYPE[sub]
35+
t = t.strip() if isinstance(t, str) and t.strip() else _type_for_dir(sub)
2736
desc = fm.get("description")
2837
desc = desc.strip() if isinstance(desc, str) else ""
2938
srcs = fm.get("sources")
@@ -34,20 +43,15 @@ def build_graph(wiki_dir: Path) -> dict:
3443
norm = {_normalize_target(nid): nid for nid in nodes}
3544
edges: list[dict] = []
3645
seen: set[tuple[str, str]] = set()
37-
for sub in _NODE_DIRS:
38-
d = wiki_dir / sub
39-
if not d.exists():
40-
continue
41-
for p in sorted(d.glob("*.md")):
42-
src = f"{sub}/{p.stem}"
43-
for raw in _extract_wikilinks(p.read_text(encoding="utf-8")):
44-
tgt = norm.get(_normalize_target(raw))
45-
if not tgt or tgt == src or (src, tgt) in seen:
46-
continue
47-
seen.add((src, tgt))
48-
edges.append({"source": src, "target": tgt})
49-
nodes[src]["out"] += 1
50-
nodes[tgt]["in"] += 1
46+
for src, text in texts.items():
47+
for raw in _extract_wikilinks(text):
48+
tgt = norm.get(_normalize_target(raw))
49+
if not tgt or tgt == src or (src, tgt) in seen:
50+
continue
51+
seen.add((src, tgt))
52+
edges.append({"source": src, "target": tgt})
53+
nodes[src]["out"] += 1
54+
nodes[tgt]["in"] += 1
5155

5256
types = sorted({n["type"] for n in nodes.values()})
5357
return {"nodes": list(nodes.values()), "edges": edges, "types": types}

0 commit comments

Comments
 (0)