|
131 | 131 | .controls .tabs button:hover{color:var(--ink)} |
132 | 132 | .controls .tabs button.on{background:rgba(56,189,248,.22);color:#bfe6ff} |
133 | 133 |
|
134 | | -/* ---- treemap infographic ---- */ |
135 | | -.treemap{position:fixed;left:0;top:58px;right:0;bottom:66px;z-index:3;overflow:hidden} |
136 | 134 | /* ---- mind-map (horizontal tree) ---- */ |
137 | 135 | .mindmap{position:fixed;left:0;top:58px;right:0;bottom:66px;z-index:3;overflow:auto} |
138 | 136 | .mindmap .mm-canvas{position:relative} |
|
157 | 155 | .mindmap .mm-caret:hover{background:rgba(255,255,255,.2);color:var(--ink)} |
158 | 156 | .mindmap::-webkit-scrollbar{width:9px;height:9px} |
159 | 157 | .mindmap::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:8px} |
160 | | -.treemap .tm-doc{position:absolute;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden} |
161 | | -.treemap .tm-h{ |
162 | | - position:absolute;left:0;top:0;right:0;height:21px;padding:0 8px; |
163 | | - font:600 .62rem/21px var(--mono);letter-spacing:.04em;color:var(--soft); |
164 | | - white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer; |
165 | | - background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.07); |
166 | | -} |
167 | | -.treemap .tm-h:hover{color:var(--ink);background:rgba(255,255,255,.08)} |
168 | | -.treemap .tm-t{ |
169 | | - position:absolute;border:1px solid;border-radius:5px;cursor:pointer;overflow:hidden; |
170 | | - display:flex;align-items:center;justify-content:center;text-align:center; |
171 | | - transition:filter .12s ease,transform .12s ease; |
172 | | -} |
173 | | -.treemap .tm-t:hover{filter:brightness(1.5);z-index:2} |
174 | | -.treemap .tm-t span{font:.66rem/1.15 var(--mono);padding:2px 4px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word} |
175 | | -.treemap .tm-empty{position:absolute;left:30px;top:30px;color:var(--mut);font:.8rem var(--mono)} |
176 | 158 |
|
177 | 159 | /* ---- glass inspector panel ---- */ |
178 | 160 | .panel{ |
|
224 | 206 | <div class="grain"></div> |
225 | 207 | <div class="vignette"></div> |
226 | 208 | <canvas id="c"></canvas> |
227 | | -<div class="treemap" id="treemap"></div> |
228 | 209 | <div class="mindmap" id="mindmap"></div> |
229 | 210 |
|
230 | 211 | <div class="brand">openkb<small>knowledge graph</small></div> |
|
242 | 223 |
|
243 | 224 | <div class="controls"> |
244 | 225 | <div class="tabs" id="modeTabs"> |
245 | | - <button data-m="mindmap">mind-map</button><button data-m="radial">radial</button><button data-m="graph">3D</button><button data-m="treemap">treemap</button> |
| 226 | + <button data-m="mindmap">mind-map</button><button data-m="radial">radial</button><button data-m="graph">3D</button> |
246 | 227 | </div> |
247 | 228 | <span class="sepc"></span> |
248 | 229 | <label id="spacingCtl">spacing <input id="spread" type="range" min="0.5" max="3.5" step="0.1" value="1.7"></label> |
|
312 | 293 | let treeEdges = []; // radial hierarchy edges (root→doc, doc→concept) |
313 | 294 | const DEFAULT_SPREAD = 1.7; // open the nebula out by default so it isn't a clump |
314 | 295 | let spread = DEFAULT_SPREAD; // spacing knob: >1 spreads nodes apart, <1 packs them tighter |
315 | | -let mode = "mindmap"; // "mindmap" = OpenKB-rooted horizontal tree (default) · "graph" = 3D nebula · "treemap" = infographic |
| 296 | +let mode = "graph"; // "graph" = 3D nebula (default) · "mindmap" = horizontal tree · "radial" = OpenKB-centred circle |
316 | 297 | let idleFrames = 0; // frames since last interaction → drives idle auto-rotation |
317 | 298 | let autoFit = true; // keep gently framing the cloud until the user takes control |
318 | 299 |
|
|
490 | 471 | alpha += (0 - alpha) * 0.02; // anneal: forces fade → the layout settles and stops |
491 | 472 | } |
492 | 473 |
|
493 | | -/* ===================== provenance tree (document → summary → concept/entity) ===================== */ |
494 | | -// The wiki's real hierarchy lives in the frontmatter: each summary's `sources` is its origin |
495 | | -// document (full_text), and each concept/entity's `sources` are the summaries it was drawn from. |
496 | | -// Treemap: documents as blocks (area ∝ their concepts' total connectivity), concepts tiled inside |
497 | | -// (tile area ∝ connectivity), coloured by type. An at-a-glance infographic; click a tile to read it. |
498 | | -const tmEl = document.getElementById("treemap"); |
499 | 474 | const spacingCtl = document.getElementById("spacingCtl"); |
500 | | -let tmQuery = ""; |
501 | | -const tmResolveSrc = s => { // "summaries/foo.md" → summary node id "summaries/foo" |
502 | | - const id = String(s).replace(/\.md$/i, ""); |
503 | | - return (byId[id] && byId[id].id.startsWith("summaries/")) ? id : null; |
504 | | -}; |
505 | | -const tmWeight = n => 1 + (n.in || 0) + (n.out || 0); // bigger = more connected |
506 | | - |
507 | | -// Squarified treemap (Bruls et al.): pack weighted items into a rect with near-square tiles. |
508 | | -function squarify(items, x, y, w, h){ |
509 | | - const out = []; |
510 | | - const ns = items.filter(it => it.w > 0).map(it => ({ it, area: 0 })); |
511 | | - if(!ns.length || w <= 0 || h <= 0) return out; |
512 | | - const total = ns.reduce((s, n) => s + n.it.w, 0); |
513 | | - const k = (w * h) / total; |
514 | | - ns.forEach(n => n.area = n.it.w * k); |
515 | | - ns.sort((a, b) => b.area - a.area); |
516 | | - const rect = { x, y, w, h }; |
517 | | - let row = []; |
518 | | - const worst = (rw, len) => { |
519 | | - if(!rw.length) return Infinity; |
520 | | - const sum = rw.reduce((s, n) => s + n.area, 0); |
521 | | - const mx = Math.max(...rw.map(n => n.area)), mn = Math.min(...rw.map(n => n.area)); |
522 | | - return Math.max((len * len * mx) / (sum * sum), (sum * sum) / (len * len * mn)); |
523 | | - }; |
524 | | - const flush = () => { |
525 | | - const sum = row.reduce((s, n) => s + n.area, 0); |
526 | | - if(rect.w >= rect.h){ |
527 | | - const cw = sum / rect.h; let yy = rect.y; |
528 | | - row.forEach(n => { const hh = n.area / cw; out.push({ ...n.it, X: rect.x, Y: yy, W: cw, H: hh }); yy += hh; }); |
529 | | - rect.x += cw; rect.w -= cw; |
530 | | - } else { |
531 | | - const rh = sum / rect.w; let xx = rect.x; |
532 | | - row.forEach(n => { const ww = n.area / rh; out.push({ ...n.it, X: xx, Y: rect.y, W: ww, H: rh }); xx += ww; }); |
533 | | - rect.y += rh; rect.h -= rh; |
534 | | - } |
535 | | - row = []; |
536 | | - }; |
537 | | - ns.forEach(n => { |
538 | | - const len = Math.min(rect.w, rect.h); |
539 | | - if(row.length && worst([...row, n], len) > worst(row, len)) flush(); |
540 | | - row.push(n); |
541 | | - }); |
542 | | - if(row.length) flush(); |
543 | | - return out; |
544 | | -} |
545 | | - |
546 | | -function buildTreemap(){ |
547 | | - const q = tmQuery.toLowerCase(); |
548 | | - const childrenOf = {}, sums = []; |
549 | | - nodes.forEach(n => { if(n.id.startsWith("summaries/")){ childrenOf[n.id] = []; sums.push(n); } }); |
550 | | - nodes.forEach(n => { |
551 | | - if(n.id.startsWith("summaries/") || !typeVisible(n.type)) return; |
552 | | - [...new Set((n.sources || []).map(tmResolveSrc).filter(Boolean))] |
553 | | - .forEach(p => { if(childrenOf[p]) childrenOf[p].push(n); }); |
554 | | - }); |
555 | | - const docItems = sums.filter(s => typeVisible(s.type)).map(s => { |
556 | | - const kids = childrenOf[s.id].filter(c => !q || c.label.toLowerCase().includes(q)); |
557 | | - return { id: s.id, label: s.label, kids, w: kids.reduce((a, c) => a + tmWeight(c), 0) }; |
558 | | - }).filter(d => d.w > 0); |
559 | | - |
560 | | - const W = tmEl.clientWidth, H = tmEl.clientHeight, PAD = 4, HEAD = 21; |
561 | | - let html = ""; |
562 | | - squarify(docItems, 0, 0, W, H).forEach(d => { |
563 | | - html += `<div class="tm-doc" style="left:${d.X}px;top:${d.Y}px;width:${d.W}px;height:${d.H}px">` |
564 | | - + `<div class="tm-h" data-id="${esc(d.id)}" title="${esc(d.label)}">${esc(d.label)}</div>`; |
565 | | - // tiles are children of the doc div → use coords RELATIVE to it (origin = PAD, HEAD) |
566 | | - const iw = d.W - 2 * PAD, ih = d.H - HEAD - PAD; |
567 | | - if(iw > 6 && ih > 6){ |
568 | | - squarify(d.kids.map(c => ({ id: c.id, label: c.label, type: c.type, w: tmWeight(c) })), PAD, HEAD, iw, ih) |
569 | | - .forEach(t => { |
570 | | - const col = colorOf(t.type), txt = t.W > 38 && t.H > 17; |
571 | | - html += `<div class="tm-t" data-id="${esc(t.id)}" title="${esc(t.label)}" ` |
572 | | - + `style="left:${t.X}px;top:${t.Y}px;width:${Math.max(0, t.W - 2)}px;height:${Math.max(0, t.H - 2)}px;` |
573 | | - + `background:rgba(${col},.2);border-color:rgba(${col},.5)">` |
574 | | - + (txt ? `<span style="color:rgb(${col})">${esc(t.label)}</span>` : ``) + `</div>`; |
575 | | - }); |
576 | | - } |
577 | | - html += `</div>`; |
578 | | - }); |
579 | | - tmEl.innerHTML = html || '<div class="tm-empty">Nothing to show.</div>'; |
580 | | - tmEl.querySelectorAll("[data-id]").forEach(el => el.onclick = () => { |
581 | | - const m = byId[el.dataset.id]; if(m) openPanel(m); |
582 | | - }); |
583 | | -} |
584 | | -const MODE_LABEL = { mindmap: "mind-map", radial: "radial", graph: "3D", treemap: "treemap" }; |
| 475 | +const MODE_LABEL = { mindmap: "mind-map", radial: "radial", graph: "3D" }; |
585 | 476 | function setMode(m){ |
586 | 477 | mode = m; |
587 | | - const canvasMode = (m === "graph" || m === "radial"); // canvas: 3D nebula + radial. mind-map & treemap are DOM |
| 478 | + const canvasMode = (m === "graph" || m === "radial"); // canvas: 3D nebula + radial. mind-map is DOM |
588 | 479 | canvas.style.display = canvasMode ? "block" : "none"; |
589 | | - tmEl.style.display = (m === "treemap") ? "block" : "none"; |
590 | 480 | mmEl.style.display = (m === "mindmap") ? "block" : "none"; |
591 | | - spacingCtl.style.display = (m === "treemap" || m === "mindmap") ? "none" : "flex"; |
| 481 | + spacingCtl.style.display = (m === "mindmap") ? "none" : "flex"; |
592 | 482 | hover = null; closePanel(); panX = 0; panY = 0; autoFit = true; |
593 | | - if(m === "treemap"){ buildTreemap(); } |
594 | | - else if(m === "mindmap"){ buildMindmap(); } |
| 483 | + if(m === "mindmap"){ buildMindmap(); } |
595 | 484 | else if(m === "radial"){ yaw = 0; pitch = 0; scale = DEF_SCALE; layoutRadial(); alpha = 1; } |
596 | 485 | else { yaw = DEF_YAW; pitch = DEF_PITCH; scale = DEF_SCALE; seed(); alpha = 1; } |
597 | 486 | [...modeTabs.children].forEach(b => b.classList.toggle("on", b.dataset.m === m)); |
|
601 | 490 | hintEl.innerHTML = |
602 | 491 | mode === "mindmap" ? 'OpenKB → documents → concepts <kbd>+/−</kbd> expand a branch <kbd>click</kbd> read details' |
603 | 492 | : mode === "radial" ? 'centre <b>OpenKB</b> → documents → concepts faint = cross-refs <kbd>click</kbd> read <kbd>drag</kbd> pan' |
604 | | - : mode === "treemap" ? 'each block = a document tiles = its concepts (size = connections) <kbd>click</kbd> read' |
605 | 493 | : '<kbd>scroll</kbd> zoom <kbd>drag bg</kbd> rotate <kbd>click</kbd> inspect<br>' |
606 | 494 | + '<kbd>drag node</kbd> pull out & pin <kbd>dbl-click</kbd> release'; |
607 | 495 | } |
|
842 | 730 | treeEdges.forEach(drawTreeEdge); |
843 | 731 | nodes.filter(visible).forEach(n => drawNode(n, t)); |
844 | 732 | drawRoot(); |
845 | | - } else if(mode === "graph"){ // mind-map & treemap are DOM — the canvas idles |
| 733 | + } else if(mode === "graph"){ // mind-map is DOM — the canvas idles in that mode |
846 | 734 | if(alpha > 0.005) step(); |
847 | 735 | project(); |
848 | 736 | if(autoFit && !orbiting && !drag){ fitStep(); } // smoothly keep the cloud framed to the viewport |
|
959 | 847 | row.onclick = () => { |
960 | 848 | row.classList.toggle("off"); |
961 | 849 | if(hiddenTypes.has(t)) hiddenTypes.delete(t); else hiddenTypes.add(t); |
962 | | - if(mode === "treemap") buildTreemap(); // rebuild without the hidden type |
963 | | - else if(mode === "mindmap") buildMindmap(); |
| 850 | + if(mode === "mindmap") buildMindmap(); // rebuild without the hidden type |
964 | 851 | else { if(mode === "radial") layoutRadial(); alpha = Math.max(alpha, 0.4); } // reheat / re-fan |
965 | 852 | }; |
966 | 853 | legend.appendChild(row); |
|
969 | 856 | /* ===================== search ===================== */ |
970 | 857 | search.addEventListener("input", e => { |
971 | 858 | const raw = e.target.value.trim(); |
972 | | - if(mode === "treemap"){ tmQuery = raw; buildTreemap(); return; } // filter tiles |
973 | 859 | if(mode === "mindmap"){ mmQuery = raw; buildMindmap(); return; } // filter branches |
974 | 860 | const q = raw.toLowerCase(); |
975 | 861 | if(!q){ hover = null; return; } |
|
995 | 881 | }); |
996 | 882 | document.getElementById("reset").addEventListener("click", () => { |
997 | 883 | hover = null; closePanel(); |
998 | | - if(mode === "treemap"){ |
999 | | - tmQuery = ""; search.value = ""; buildTreemap(); |
1000 | | - } else if(mode === "mindmap"){ |
| 884 | + if(mode === "mindmap"){ |
1001 | 885 | mmQuery = ""; search.value = ""; mmCollapsed.clear(); collapseDocs(); buildMindmap(); mmEl.scrollTop = 0; mmEl.scrollLeft = 0; |
1002 | 886 | } else if(mode === "radial"){ |
1003 | 887 | spread = DEFAULT_SPREAD; spreadEl.value = String(DEFAULT_SPREAD); panX = 0; panY = 0; |
|
1013 | 897 |
|
1014 | 898 | /* ===================== boot ===================== */ |
1015 | 899 | function collapseDocs(){ nodes.forEach(n => { if(n.id.startsWith("summaries/")) mmCollapsed.add(n.id); }); } // start compact: docs folded |
1016 | | -window.addEventListener("resize", () => { size(); autoFit = true; if(mode === "treemap") buildTreemap(); }); |
| 900 | +window.addEventListener("resize", () => { size(); autoFit = true; if(mode === "mindmap") buildMindmap(); }); |
1017 | 901 | size(); seed(); collapseDocs(); |
1018 | | -setMode("mindmap"); // open on the OpenKB-rooted mind-map |
| 902 | +setMode("graph"); // open on the 3D nebula |
1019 | 903 | requestAnimationFrame(frame); |
1020 | 904 | </script> |
1021 | 905 | </body> |
|
0 commit comments