Skip to content

Commit 0a5cc4a

Browse files
committed
feat(visualize): horizontal collapsible mind-map (XMind-style) as default
Replace the radial 'circle' layout with a proper left-to-right tree, which reads unambiguously as a mind-map: - OpenKB root on the left → documents → each document's concepts/entities, laid out as a tidy tree with curved (bezier) connectors (DOM + SVG) - documents start collapsed with a +N badge; click +/- to expand a branch, click a node to read its details/sources/links - search filters branches, legend filters types, reset re-collapses - view button cycles mind-map -> 3D nebula -> treemap Removes the canvas radial mind-map.
1 parent be3b597 commit 0a5cc4a

1 file changed

Lines changed: 107 additions & 114 deletions

File tree

openkb/templates/graph.html

Lines changed: 107 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,30 @@
125125

126126
/* ---- treemap infographic ---- */
127127
.treemap{position:fixed;left:0;top:58px;right:0;bottom:66px;z-index:3;overflow:hidden}
128+
/* ---- mind-map (horizontal tree) ---- */
129+
.mindmap{position:fixed;left:0;top:58px;right:0;bottom:66px;z-index:3;overflow:auto}
130+
.mindmap .mm-canvas{position:relative}
131+
.mindmap .mm-links{position:absolute;left:0;top:0;pointer-events:none}
132+
.mindmap .mm-node{
133+
position:absolute;width:184px;height:20px;display:flex;align-items:center;gap:7px;
134+
padding:0 9px;border:1px solid var(--line);border-radius:11px;background:var(--glass);
135+
font:.72rem var(--mono);color:var(--ink);cursor:pointer;box-sizing:border-box;
136+
transition:filter .12s ease;
137+
}
138+
.mindmap .mm-node:hover{filter:brightness(1.4)}
139+
.mindmap .mm-root{
140+
width:86px;justify-content:center;font-weight:700;color:#0a0e14;
141+
background:rgba(232,242,252,.96);border:none;box-shadow:0 0 16px rgba(120,200,255,.6);
142+
}
143+
.mindmap .mm-dot{width:8px;height:8px;border-radius:50%;flex:none}
144+
.mindmap .mm-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
145+
.mindmap .mm-caret{
146+
flex:none;min-width:15px;text-align:center;color:var(--soft);font-size:.64rem;
147+
background:rgba(255,255,255,.09);border-radius:7px;padding:0 4px;
148+
}
149+
.mindmap .mm-caret:hover{background:rgba(255,255,255,.2);color:var(--ink)}
150+
.mindmap::-webkit-scrollbar{width:9px;height:9px}
151+
.mindmap::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:8px}
128152
.treemap .tm-doc{position:absolute;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden}
129153
.treemap .tm-h{
130154
position:absolute;left:0;top:0;right:0;height:21px;padding:0 8px;
@@ -193,6 +217,7 @@
193217
<div class="vignette"></div>
194218
<canvas id="c"></canvas>
195219
<div class="treemap" id="treemap"></div>
220+
<div class="mindmap" id="mindmap"></div>
196221

197222
<div class="brand">openkb<small>knowledge graph</small></div>
198223

@@ -246,8 +271,7 @@
246271
r: 3.5 + Math.min(18, Math.sqrt((n.in || 0) + (n.out || 0)) * 2.1), // sqrt-scaled by degree → hubs read big, leaves stay small
247272
col: colorOf(n.type), // cached color → no per-frame lookup
248273
alpha: 1, // per-node fade (legend toggle)
249-
sx:0, sy:0, rz:0, pp:1, hl:1, pinned:false, // projection cache + smoothed highlight + pinned (drag to pull out)
250-
tx:0, ty:0, show:true, depth:0 // mind-map: radial target, visibility, layer
274+
sx:0, sy:0, rz:0, pp:1, hl:1, pinned:false // projection cache + smoothed highlight + pinned (drag to pull out)
251275
}));
252276
const byId = Object.fromEntries(nodes.map(n => [n.id, n]));
253277
const edges = GRAPH.edges.filter(e => byId[e.source] && byId[e.target]);
@@ -268,16 +292,11 @@
268292
let scale = DEF_SCALE, panX = 0, panY = 0;
269293
let yaw = DEF_YAW, pitch = DEF_PITCH; // 3D orbit angles — drag the background to rotate
270294
let orbiting = false, oStartX = 0, oStartY = 0, yawStart = 0, pitchStart = 0;
271-
let panOrigX = 0, panOrigY = 0, pendingInspect = null; // mind-map: bg-drag pan + click-to-inspect
272295
let didDrag = false;
273296
let alpha = 1; // simulated-annealing "temperature"; cools to 0 so motion stops
274297
const DEFAULT_SPREAD = 1.7; // open the nebula out by default so it isn't a clump
275298
let spread = DEFAULT_SPREAD; // spacing knob: >1 spreads nodes apart, <1 packs them tighter
276-
let mode = "mindmap"; // "mindmap" = OpenKB-rooted radial tree (default) · "graph" = 3D nebula · "treemap" = infographic
277-
// synthetic centre node for the mind-map ("OpenKB" → documents → concepts)
278-
const ROOT = { id:"__openkb__", label:"OpenKB", type:"__root__", x:0, y:0, z:0, tx:0, ty:0,
279-
sx:0, sy:0, rz:0, pp:1, r:15, hl:1, alpha:1, depth:0, show:true };
280-
let treeEdges = []; // mind-map hierarchy edges (root→doc, doc→concept)
299+
let mode = "mindmap"; // "mindmap" = OpenKB-rooted horizontal tree (default) · "graph" = 3D nebula · "treemap" = infographic
281300
let idleFrames = 0; // frames since last interaction → drives idle auto-rotation
282301
let autoFit = true; // keep gently framing the cloud until the user takes control
283302

@@ -303,54 +322,80 @@
303322
});
304323
}
305324

306-
/* ===================== mind-map: OpenKB → documents → concepts (radial tree) ===================== */
307-
// concept/entity `sources` like "summaries/foo.md" → summary node id "summaries/foo"
325+
/* ===================== mind-map: OpenKB → documents → concepts (horizontal tree, DOM) ===================== */
326+
const mmEl = document.getElementById("mindmap");
327+
const mmCollapsed = new Set(); // node ids whose children are hidden
328+
let mmQuery = "";
308329
const mmResolveSrc = s => { const id = String(s).replace(/\.md$/i, ""); return (byId[id] && byId[id].id.startsWith("summaries/")) ? id : null; };
309-
function layoutMindmap(){
330+
const ROOT_W = 86, NODE_W = 184, COL = 234, ROW = 27, PADX = 34, PADY = 28, NH = 20;
331+
function buildMindmap(){
332+
const q = mmQuery.toLowerCase();
333+
const keep = n => !q || n.label.toLowerCase().includes(q) || n.id.toLowerCase().includes(q);
310334
const docs = nodes.filter(n => n.id.startsWith("summaries/") && typeVisible(n.type));
311335
const kidsOf = {}; docs.forEach(d => kidsOf[d.id] = []);
312-
const rootDirect = []; // concepts with no resolvable document → hang off the centre
336+
const rootDirect = [];
313337
nodes.forEach(n => {
314-
if(n.id.startsWith("summaries/") || !typeVisible(n.type)){ n.show = false; return; }
338+
if(n.id.startsWith("summaries/") || !typeVisible(n.type)) return;
315339
const p = (n.sources || []).map(mmResolveSrc).filter(Boolean)[0]; // primary document
316340
if(p && kidsOf[p]) kidsOf[p].push(n); else rootDirect.push(n);
317341
});
318-
nodes.forEach(n => { n.show = false; });
319-
const branches = docs.map(d => ({ node: d, kids: kidsOf[d.id], w: Math.max(1.6, kidsOf[d.id].length) }))
320-
.concat(rootDirect.map(c => ({ node: c, kids: [], w: 1 })));
321-
const totW = branches.reduce((s, b) => s + b.w, 0) || 1;
322-
const R1 = 250 * (0.6 + 0.4 * spread), R2 = R1 + 175 * (0.6 + 0.4 * spread);
323-
treeEdges = [];
324-
ROOT.show = true; ROOT.tx = 0; ROOT.ty = 0;
325-
let a = -Math.PI;
326-
branches.forEach(b => {
327-
const wedge = 2 * Math.PI * b.w / totW, mid = a + wedge / 2;
328-
b.node.show = true; b.node.depth = 1;
329-
b.node.tx = Math.cos(mid) * R1; b.node.ty = Math.sin(mid) * R1;
330-
treeEdges.push({ a: ROOT, b: b.node });
331-
b.kids.forEach((c, i) => {
332-
const ca = a + wedge * (i + 0.5) / Math.max(1, b.kids.length);
333-
c.show = true; c.depth = 2;
334-
c.tx = Math.cos(ca) * R2; c.ty = Math.sin(ca) * R2;
335-
treeEdges.push({ a: b.node, b: c });
336-
});
337-
a += wedge;
342+
const mk = n => ({ id:n.id, label:n.label, type:n.type, kids:[] });
343+
const root = { id:"__openkb__", label:"OpenKB", type:"__root__", kids:[] };
344+
docs.forEach(d => {
345+
const kids = kidsOf[d.id].filter(keep).map(mk);
346+
if(q && !keep(d) && !kids.length) return; // search prunes empty branches
347+
const dn = mk(d); dn.kids = kids; root.kids.push(dn);
338348
});
339-
}
340-
function stepMindmap(){
341-
for(const n of nodes){
342-
n.alpha += ((n.show && typeVisible(n.type) ? 1 : 0) - n.alpha) * 0.14;
343-
if(n.show){ n.x += (n.tx - n.x) * 0.16; n.y += (n.ty - n.y) * 0.16; n.z += (0 - n.z) * 0.16; }
344-
}
345-
ROOT.x += (0 - ROOT.x) * 0.16; ROOT.y += (0 - ROOT.y) * 0.16;
346-
alpha += (0 - alpha) * 0.02;
349+
rootDirect.filter(keep).forEach(c => root.kids.push(mk(c)));
350+
351+
let rows = 0, maxDepth = 0;
352+
const flat = [], links = [];
353+
(function place(item, depth){
354+
maxDepth = Math.max(maxDepth, depth);
355+
item.x = PADX + depth * COL;
356+
item.w = item.id === "__openkb__" ? ROOT_W : NODE_W;
357+
const expanded = item.kids.length && !mmCollapsed.has(item.id);
358+
if(expanded){
359+
item.kids.forEach(k => place(k, depth + 1));
360+
item.y = (item.kids[0].y + item.kids[item.kids.length - 1].y) / 2;
361+
item.kids.forEach(k => links.push([item, k]));
362+
} else {
363+
item.y = PADY + rows * ROW; rows++;
364+
}
365+
flat.push(item);
366+
})(root, 0);
367+
368+
const Wpx = PADX + (maxDepth + 1) * COL + 30, Hpx = PADY + rows * ROW + 30;
369+
let svg = `<svg class="mm-links" width="${Wpx}" height="${Hpx}">`;
370+
links.forEach(([p, c]) => {
371+
const x1 = p.x + p.w, y1 = p.y + NH/2, x2 = c.x, y2 = c.y + NH/2, mx = (x1 + x2) / 2;
372+
svg += `<path d="M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}" fill="none" stroke="rgba(255,255,255,.15)" stroke-width="1.3"/>`;
373+
});
374+
svg += `</svg>`;
375+
let html = "";
376+
flat.forEach(it => {
377+
if(it.id === "__openkb__"){ html += `<div class="mm-node mm-root" style="left:${it.x}px;top:${it.y}px">OpenKB</div>`; return; }
378+
const col = colorOf(it.type), kn = it.kids.length || (mmCollapsed.has(it.id) ? 1 : 0);
379+
const hasKids = it.id.startsWith("summaries/") && (kidsOf[it.id] || []).length;
380+
html += `<div class="mm-node" data-id="${esc(it.id)}" title="${esc(it.label)}" `
381+
+ `style="left:${it.x}px;top:${it.y}px;border-color:rgba(${col},.5);background:rgba(${col},.12)">`
382+
+ `<span class="mm-dot" style="background:rgb(${col})"></span><span class="mm-label">${esc(it.label)}</span>`
383+
+ (hasKids ? `<span class="mm-caret" data-tog="${esc(it.id)}">${mmCollapsed.has(it.id) ? "+" + (kidsOf[it.id] || []).length : "−"}</span>` : ``)
384+
+ `</div>`;
385+
});
386+
mmEl.innerHTML = `<div class="mm-canvas" style="width:${Wpx}px;height:${Hpx}px">${svg}${html}</div>`;
387+
mmEl.querySelectorAll("[data-tog]").forEach(el => el.onclick = e => {
388+
e.stopPropagation(); const id = el.dataset.tog;
389+
if(mmCollapsed.has(id)) mmCollapsed.delete(id); else mmCollapsed.add(id);
390+
buildMindmap();
391+
});
392+
mmEl.querySelectorAll(".mm-node[data-id]").forEach(el => el.onclick = () => { const m = byId[el.dataset.id]; if(m) openPanel(m); });
347393
}
348394

349395
/* ===================== physics (adapted from okf-spec.html step()) ===================== */
350396
const VMAX = 9; // per-frame speed cap → no violent swings, even on a dense hub
351397
const alphaTarget = n => typeVisible(n.type) ? 1 : 0;
352398
function step(){
353-
if(mode === "mindmap"){ stepMindmap(); return; }
354399
for(let i=0;i<nodes.length;i++){
355400
const a = nodes[i];
356401
if(!visible(a)) continue;
@@ -484,20 +529,21 @@
484529
const MODE_LABEL = { mindmap: "mind-map", graph: "3D nebula", treemap: "treemap" };
485530
function setMode(m){
486531
mode = m;
487-
const tm = (m === "treemap");
488-
canvas.style.display = tm ? "none" : "block";
489-
tmEl.style.display = tm ? "block" : "none";
490-
spacingCtl.style.display = tm ? "none" : "flex";
532+
const canvasMode = (m === "graph"); // only the 3D nebula uses the canvas; mind-map & treemap are DOM
533+
canvas.style.display = canvasMode ? "block" : "none";
534+
tmEl.style.display = (m === "treemap") ? "block" : "none";
535+
mmEl.style.display = (m === "mindmap") ? "block" : "none";
536+
spacingCtl.style.display = canvasMode ? "flex" : "none";
491537
hover = null; closePanel(); panX = 0; panY = 0; autoFit = true;
492538
if(m === "treemap"){ buildTreemap(); }
493-
else if(m === "mindmap"){ yaw = 0; pitch = 0; scale = DEF_SCALE; layoutMindmap(); alpha = 1; }
539+
else if(m === "mindmap"){ buildMindmap(); }
494540
else { yaw = DEF_YAW; pitch = DEF_PITCH; scale = DEF_SCALE; seed(); alpha = 1; }
495541
modeBtn.textContent = MODE_LABEL[m];
496542
updateHint();
497543
}
498544
function updateHint(){
499545
hintEl.innerHTML =
500-
mode === "mindmap" ? 'centre <b>OpenKB</b> → documents → concepts &nbsp; faint lines = cross-refs &nbsp; <kbd>click</kbd> read &nbsp; <kbd>drag</kbd> pan &nbsp; <kbd>scroll</kbd> zoom'
546+
mode === "mindmap" ? 'OpenKB → documents → concepts &nbsp; <kbd>+/−</kbd> expand a branch &nbsp; <kbd>click</kbd> read details'
501547
: mode === "treemap" ? 'each block = a document &nbsp; tiles = its concepts (size = connections) &nbsp; <kbd>click</kbd> read'
502548
: '<kbd>scroll</kbd> zoom &nbsp; <kbd>drag bg</kbd> rotate &nbsp; <kbd>click</kbd> inspect<br>'
503549
+ '<kbd>drag node</kbd> pull out &amp; pin &nbsp; <kbd>dbl-click</kbd> release';
@@ -580,37 +626,6 @@
580626
return best;
581627
}
582628

583-
/* ===================== mind-map rendering ===================== */
584-
function drawCrossEdge(e){ // faint wikilink "cross-reference"; lights up on hover
585-
const a = byId[e.source], b = byId[e.target];
586-
if(!a.show || !b.show || a.alpha < 0.05 || b.alpha < 0.05) return;
587-
const hot = hover && (e.source === hover || e.target === hover);
588-
ctx.globalAlpha = hot ? 0.85 : 0.05;
589-
ctx.strokeStyle = hot ? "rgba(45,212,191,.9)" : "rgba(150,180,225,.6)";
590-
ctx.lineWidth = hot ? 1.4 : 0.6;
591-
ctx.beginPath(); ctx.moveTo(a.sx, a.sy); ctx.lineTo(b.sx, b.sy); ctx.stroke();
592-
ctx.globalAlpha = 1;
593-
}
594-
function drawTreeEdge(e){ // bright hierarchy spoke (root→doc, doc→concept)
595-
const a = e.a, b = e.b;
596-
if(b.alpha < 0.05) return;
597-
ctx.globalAlpha = 0.5 * (b.alpha || 1);
598-
ctx.strokeStyle = "rgba(255,255,255,.34)";
599-
ctx.lineWidth = (a === ROOT) ? 1.7 : 1.0;
600-
ctx.beginPath(); ctx.moveTo(a.sx, a.sy); ctx.lineTo(b.sx, b.sy); ctx.stroke();
601-
ctx.globalAlpha = 1;
602-
}
603-
function drawRoot(){
604-
const x = ROOT.sx, y = ROOT.sy, r = ROOT.r;
605-
ctx.beginPath(); ctx.arc(x, y, r, 0, TAU);
606-
ctx.fillStyle = "rgba(232,242,252,.96)";
607-
ctx.shadowColor = "rgba(120,200,255,.95)"; ctx.shadowBlur = 24; ctx.fill(); ctx.shadowBlur = 0;
608-
ctx.fillStyle = "rgba(238,242,247,.98)"; ctx.font = "700 13px ui-monospace,monospace"; ctx.textAlign = "center";
609-
ctx.shadowColor = "rgba(4,6,10,.95)"; ctx.shadowBlur = 5;
610-
ctx.fillText("OpenKB", x, y + r + 16);
611-
ctx.shadowBlur = 0;
612-
}
613-
614629
/* ===================== rendering ===================== */
615630
function drawEdge(e, t){
616631
const a = byId[e.source], b = byId[e.target];
@@ -709,7 +724,7 @@
709724
}
710725
// (flat discs — no glossy core highlight)
711726
// label — declutter: hubs at rest; hovered node + neighbors; selection; zoomed in; near depth only
712-
const showLabel = (mode === "mindmap" && n.depth <= 1) || (scale > 1.4) || (n.id === selected) || (hover ? hl > 0.55 : labelIds.has(n.id));
727+
const showLabel = (scale > 1.4) || (n.id === selected) || (hover ? hl > 0.55 : labelIds.has(n.id));
713728
if(showLabel && fog > 0.55){
714729
ctx.fillStyle = `rgba(238,242,247,${0.4 + 0.55*hl})`;
715730
ctx.font = `600 11px ui-monospace,monospace`;
@@ -723,23 +738,7 @@
723738
}
724739

725740
function frame(t){
726-
if(mode === "mindmap"){
727-
if(alpha > 0.005) step();
728-
project(); // flat (yaw=pitch=0)
729-
ROOT.pp = 1; ROOT.rz = 0;
730-
ROOT.sx = W()/2 + panX + ROOT.x*scale; ROOT.sy = H()/2 + panY + ROOT.y*scale;
731-
if(autoFit && !orbiting) fitStep();
732-
const focus = hover || selected;
733-
for(const n of nodes){
734-
const near = !focus || n.id===focus || (adj[focus] && adj[focus].has(n.id));
735-
n.hl += ((near ? 1 : 0.5) - n.hl) * 0.12; // gentle dim — keep the whole tree readable
736-
}
737-
ctx.setTransform(DPR,0,0,DPR,0,0); ctx.clearRect(0,0,W(),H());
738-
edges.forEach(drawCrossEdge); // faint cross-references (+ hovered node's lit)
739-
treeEdges.forEach(drawTreeEdge); // bright hierarchy
740-
nodes.filter(visible).forEach(n => drawNode(n, t));
741-
drawRoot();
742-
} else if(mode === "graph"){ // treemap is DOM — the canvas idles
741+
if(mode === "graph"){ // mind-map & treemap are DOM — the canvas idles
743742
if(alpha > 0.005) step();
744743
project();
745744
if(autoFit && !orbiting && !drag){ fitStep(); } // smoothly keep the cloud framed to the viewport
@@ -771,14 +770,9 @@
771770
alpha = Math.max(alpha, 0.5); // reheat so neighbors re-settle around the dragged node
772771
return;
773772
}
774-
if(orbiting){
775-
if(mode === "mindmap"){ // flat tree → drag background to pan
776-
panX = panOrigX + (e.offsetX - oStartX);
777-
panY = panOrigY + (e.offsetY - oStartY);
778-
} else { // 3D → drag background to orbit
779-
yaw = yawStart + (e.offsetX - oStartX) * 0.0045;
780-
pitch = Math.max(-1.45, Math.min(1.45, pitchStart - (e.offsetY - oStartY) * 0.0045));
781-
}
773+
if(orbiting){ // 3D → drag background to orbit
774+
yaw = yawStart + (e.offsetX - oStartX) * 0.0045;
775+
pitch = Math.max(-1.45, Math.min(1.45, pitchStart - (e.offsetY - oStartY) * 0.0045));
782776
didDrag = true; return;
783777
}
784778
const n = pick(e.offsetX, e.offsetY);
@@ -788,19 +782,13 @@
788782
const n = pick(e.offsetX, e.offsetY);
789783
didDrag = false; idleFrames = 0; autoFit = false; // user takes control → stop auto-framing
790784
oStartX = e.offsetX; oStartY = e.offsetY;
791-
if(mode === "mindmap"){ // click a node → inspect; drag bg → pan
792-
pendingInspect = n; orbiting = true; panOrigX = panX; panOrigY = panY;
793-
canvas.style.cursor = "grabbing"; return;
794-
}
795785
if(n){ drag = n; hover = n.id; canvas.style.cursor = "grabbing"; }
796786
else { orbiting = true; yawStart = yaw; pitchStart = pitch; canvas.style.cursor = "grabbing"; }
797787
});
798788
window.addEventListener("mouseup", e => {
799-
if(mode === "mindmap"){
800-
if(pendingInspect && !didDrag) openPanel(pendingInspect);
801-
} else if(drag && !didDrag){ openPanel(drag); } // click (no drag) → inspect
789+
if(drag && !didDrag){ openPanel(drag); } // click (no drag) → inspect
802790
else if(drag && didDrag){ drag.pinned = true; alpha = Math.max(alpha, 0.5); } // pulled out → pin it
803-
drag = null; orbiting = false; pendingInspect = null;
791+
drag = null; orbiting = false;
804792
canvas.style.cursor = "grab";
805793
});
806794
canvas.addEventListener("dblclick", e => {
@@ -857,6 +845,7 @@
857845
row.classList.toggle("off");
858846
if(hiddenTypes.has(t)) hiddenTypes.delete(t); else hiddenTypes.add(t);
859847
if(mode === "treemap") buildTreemap(); // rebuild without the hidden type
848+
else if(mode === "mindmap") buildMindmap();
860849
else alpha = Math.max(alpha, 0.4); // graph: reheat so the layout re-settles
861850
};
862851
legend.appendChild(row);
@@ -866,6 +855,7 @@
866855
search.addEventListener("input", e => {
867856
const raw = e.target.value.trim();
868857
if(mode === "treemap"){ tmQuery = raw; buildTreemap(); return; } // filter tiles
858+
if(mode === "mindmap"){ mmQuery = raw; buildMindmap(); return; } // filter branches
869859
const q = raw.toLowerCase();
870860
if(!q){ hover = null; return; }
871861
const m = nodes.find(n => n.label.toLowerCase().includes(q))
@@ -892,6 +882,8 @@
892882
hover = null; closePanel();
893883
if(mode === "treemap"){
894884
tmQuery = ""; search.value = ""; buildTreemap();
885+
} else if(mode === "mindmap"){
886+
mmQuery = ""; search.value = ""; mmCollapsed.clear(); collapseDocs(); buildMindmap(); mmEl.scrollTop = 0; mmEl.scrollLeft = 0;
895887
} else {
896888
spread = DEFAULT_SPREAD; spreadEl.value = String(DEFAULT_SPREAD); panX = 0; panY = 0;
897889
scale = DEF_SCALE; yaw = DEF_YAW; pitch = DEF_PITCH;
@@ -901,8 +893,9 @@
901893
});
902894

903895
/* ===================== boot ===================== */
896+
function collapseDocs(){ nodes.forEach(n => { if(n.id.startsWith("summaries/")) mmCollapsed.add(n.id); }); } // start compact: docs folded
904897
window.addEventListener("resize", () => { size(); autoFit = true; if(mode === "treemap") buildTreemap(); });
905-
size(); seed();
898+
size(); seed(); collapseDocs();
906899
setMode("mindmap"); // open on the OpenKB-rooted mind-map
907900
requestAnimationFrame(frame);
908901
</script>

0 commit comments

Comments
 (0)