Skip to content

Instantly share code, notes, and snippets.

@yohanesnuwara
Created April 6, 2026 16:03
Show Gist options
  • Select an option

  • Save yohanesnuwara/ac5a92055656fa35bf3386ad0aa71420 to your computer and use it in GitHub Desktop.

Select an option

Save yohanesnuwara/ac5a92055656fa35bf3386ad0aa71420 to your computer and use it in GitHub Desktop.
Geo-Documents Knowledge Graph
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Knowledge Graph Query</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=DM+Sans:wght@300;400;500;600;700&display=swap');
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'DM Sans',sans-serif;background:#0a0e17;color:#c8cdd3;overflow:hidden;height:100vh}
.wrap{display:flex;height:100vh}
.panel{width:380px;background:#0f1420;border-right:1px solid #1a2035;display:flex;flex-direction:column;overflow:hidden}
.panel-head{padding:16px 20px;border-bottom:1px solid #1a2035}
.panel-head h1{font-family:'JetBrains Mono',monospace;font-size:13px;color:#5eead4;letter-spacing:1px;text-transform:uppercase;margin-bottom:4px}
.panel-head p{font-size:11px;color:#64748b}
.search-box{padding:12px 16px;border-bottom:1px solid #1a2035}
.search-row{display:flex;gap:6px}
.search-row input{flex:1;padding:8px 12px;background:#1a2035;border:1px solid #2a3050;border-radius:6px;color:#f1f5f9;font-size:13px;outline:none;font-family:'DM Sans',sans-serif}
.search-row input:focus{border-color:#5eead4}
.search-row button{padding:8px 14px;background:#0d9488;border:none;border-radius:6px;color:#fff;font-size:12px;cursor:pointer;font-weight:600}
.search-row button:hover{background:#0f766e}
.samples{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}
.samples button{padding:3px 8px;background:#1a2035;border:1px solid #2a3050;border-radius:10px;color:#94a3b8;font-size:9px;cursor:pointer;font-family:'DM Sans',sans-serif}
.samples button:hover{border-color:#5eead4;color:#5eead4}
.results{flex:1;overflow-y:auto;padding:12px 16px}
.results::-webkit-scrollbar{width:4px}
.results::-webkit-scrollbar-thumb{background:#2a3050;border-radius:2px}
.step-title{font-size:10px;color:#5eead4;font-family:'JetBrains Mono',monospace;margin:12px 0 6px;letter-spacing:.5px}
.step-title:first-child{margin-top:0}
.kw{display:inline-block;padding:2px 8px;background:#164e63;border-radius:10px;font-size:11px;color:#5eead4;margin:2px}
.cypher-box{background:#1a2035;padding:10px 12px;border-radius:6px;font-size:10px;font-family:'JetBrains Mono',monospace;color:#94a3b8;line-height:1.5;border:1px solid #2a3050;white-space:pre-wrap;margin-bottom:4px}
.match-card{display:flex;align-items:center;gap:8px;padding:6px 10px;background:#1a2035;border-radius:6px;margin-bottom:3px;border-left:3px solid #555}
.match-card .mc-label{font-size:12px;color:#f1f5f9;flex:1}
.match-card .mc-meta{font-size:9px;color:#64748b;font-family:'JetBrains Mono',monospace}
.match-card .mc-score{font-size:16px;font-family:'JetBrains Mono',monospace;color:#5eead4;font-weight:700}
.hop-item{display:flex;align-items:center;gap:6px;padding:3px 8px;margin-bottom:2px;font-size:11px}
.hop-item .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.hop-item .etype{font-size:9px;font-family:'JetBrains Mono',monospace;padding:1px 5px;border-radius:8px}
.etype-semantic{color:#5eead4;background:#0d3d3d}
.etype-bridge{color:#f59e0b;background:#3d2f0d}
.etype-related{color:#818cf8;background:#2d2d4d}
.canvas-wrap{flex:1;position:relative}
canvas{width:100%;height:100%}
.tooltip{position:absolute;background:#1a2035;border:1px solid #2a3050;border-radius:8px;padding:8px 12px;font-size:11px;pointer-events:none;opacity:0;transition:opacity .12s;max-width:260px;box-shadow:0 8px 20px rgba(0,0,0,.5);z-index:10}
.tooltip .tt-l{font-weight:600;color:#f1f5f9;margin-bottom:2px}
.tooltip .tt-m{font-size:9px;color:#64748b;font-family:'JetBrains Mono',monospace}
.legend-bar{position:absolute;bottom:12px;left:12px;display:flex;gap:12px;font-size:10px;color:#64748b;background:#0f1420cc;padding:6px 12px;border-radius:6px;border:1px solid #1a2035}
.legend-bar span{display:flex;align-items:center;gap:4px}
.legend-bar .ld{width:8px;height:8px;border-radius:50%}
.legend-bar .le{width:16px;height:2px;border-radius:1px}
.no-query{color:#475569;font-size:12px;padding:20px 0;text-align:center}
</style>
</head>
<body>
<div class="wrap">
<div class="panel">
<div class="panel-head">
<h1>⬡ Geo-Documents Knowledge Graph</h1>
<p>Type a query and let the knowledge graph search the relevant documents</p>
</div>
<div class="search-box">
<div class="search-row">
<input id="qinput" placeholder="e.g. gas property of north sea reservoir" />
<button onclick="runQuery()">Query</button>
</div>
<div class="samples" id="samples"></div>
</div>
<div class="results" id="results">
<div class="no-query">Type a query or click a sample above</div>
</div>
</div>
<div class="canvas-wrap">
<canvas id="c"></canvas>
<div class="tooltip" id="tip"></div>
<div class="legend-bar">
<span><div class="ld" style="background:#e74c3c"></div>2012-GC-NTNU-Thesis.pdf</span>
<span><div class="ld" style="background:#3498db"></div>Paradigm Shift in International Gas Market Towards an Integrated Market.pdf</span>
<span><div class="ld" style="background:#2ecc71"></div>The North Sea reservoir chalk characterization.pdf</span>
<span><div class="ld" style="background:#f39c12"></div>Deepwater drilling problems.pdf</span>
<span><div class="ld" style="background:#9b59b6"></div>Evaluation of kick and loss scenarios in experimental lab facility.pdf</span>
<span><div class="ld" style="background:#1abc9c"></div>Seismic characterization of lower cretaceous clastic wedges in the Tromsø Basin.pdf</span>
<span>|</span>
<span><div class="le" style="background:#5eead4"></div>semantic</span>
<span><div class="le" style="background:#f59e0b"></div>bridge</span>
<span><div class="le" style="background:#6366f1"></div>related</span>
</div>
</div>
</div>
<script>
const DATA = {"nodes":[{"id":0,"label":"Global Gas Market & LNG Trade","size":243,"file":"Gas Market","color":"#3498db","x":15.55,"y":-1.17},{"id":1,"label":"Pumprate Experimental Data Plots","size":17,"file":"Kick & Loss Lab","color":"#9b59b6","x":-2.46,"y":21.14},{"id":2,"label":"Thesis Front Matter & Acknowledgements","size":10,"file":"GC Chromatography","color":"#e74c3c","x":1.14,"y":-2.23},{"id":3,"label":"Troms\u00f8 Basin Seismic Interpretation","size":150,"file":"Troms\u00f8 Seismic","color":"#1abc9c","x":-3.2,"y":6.07},{"id":4,"label":"Gas Kick Detection Calculations","size":14,"file":"Kick & Loss Lab","color":"#9b59b6","x":-0.2,"y":16.99},{"id":5,"label":"Chalk Geochemistry Raw Data","size":8,"file":"Chalk Reservoir","color":"#2ecc71","x":-0.13,"y":1.66},{"id":6,"label":"North Sea Chalk Characterization","size":172,"file":"Chalk Reservoir","color":"#2ecc71","x":-0.46,"y":3.25},{"id":7,"label":"Well Control & Kick Detection","size":10,"file":"Kick & Loss Lab","color":"#9b59b6","x":-0.87,"y":17.31},{"id":8,"label":"GCxGC Modulator Parameters","size":22,"file":"GC Chromatography","color":"#e74c3c","x":1.08,"y":-5.08},{"id":9,"label":"Slimhole & Deepwater Drilling","size":24,"file":"Deepwater Drilling","color":"#f39c12","x":-1.56,"y":16.26},{"id":10,"label":"Lost Circulation Experiments","size":31,"file":"Kick & Loss Lab","color":"#9b59b6","x":-1.57,"y":19.44},{"id":11,"label":"Influx Experimental Data Series","size":18,"file":"Kick & Loss Lab","color":"#9b59b6","x":-0.97,"y":19.43},{"id":12,"label":"MATLAB Scripts & Calculations","size":11,"file":"Kick & Loss Lab","color":"#9b59b6","x":-1.55,"y":19.99},{"id":13,"label":"Drilling Facilities & Rig Types","size":20,"file":"Deepwater Drilling","color":"#f39c12","x":-0.69,"y":15.07},{"id":14,"label":"Managed Pressure Drilling (MPD)","size":10,"file":"Kick & Loss Lab","color":"#9b59b6","x":-1.19,"y":17.89},{"id":15,"label":"Kick/Loss Lab Overview & Conclusions","size":17,"file":"Kick & Loss Lab","color":"#9b59b6","x":-1.25,"y":18.34},{"id":16,"label":"MPD Choke Influx Experiments","size":9,"file":"Kick & Loss Lab","color":"#9b59b6","x":-0.93,"y":18.75},{"id":17,"label":"Pressure Response to Kick Sizes","size":12,"file":"Kick & Loss Lab","color":"#9b59b6","x":-0.49,"y":18.63},{"id":18,"label":"Deepwater Drilling Concepts & Risers","size":31,"file":"Deepwater Drilling","color":"#f39c12","x":-1.18,"y":15.46},{"id":19,"label":"Riser Collapse & Burst Analysis","size":13,"file":"Deepwater Drilling","color":"#f39c12","x":0.09,"y":15.96},{"id":20,"label":"Deepwater Environment Conditions","size":9,"file":"Deepwater Drilling","color":"#f39c12","x":-1.87,"y":14.67},{"id":21,"label":"Hydrate Formation & Prevention","size":15,"file":"Deepwater Drilling","color":"#f39c12","x":-1.84,"y":14.23},{"id":22,"label":"Riser Margin Calculations","size":17,"file":"Deepwater Drilling","color":"#f39c12","x":0.24,"y":15.52},{"id":23,"label":"Riser Loss & Well Barriers","size":16,"file":"Deepwater Drilling","color":"#f39c12","x":-0.33,"y":15.77},{"id":24,"label":"Hydrocarbon & PAH Standards","size":23,"file":"GC Chromatography","color":"#e74c3c","x":2.65,"y":-2.48},{"id":25,"label":"Sulphur Standard Calibration","size":14,"file":"GC Chromatography","color":"#e74c3c","x":3.32,"y":-2.48},{"id":26,"label":"Vacuum Gas Oil Sulphur Analysis","size":9,"file":"GC Chromatography","color":"#e74c3c","x":3.06,"y":-3.06},{"id":27,"label":"Oil Composition & Refining","size":16,"file":"GC Chromatography","color":"#e74c3c","x":2.69,"y":-3.19},{"id":28,"label":"GC Inlet & Injection Methods","size":8,"file":"GC Chromatography","color":"#e74c3c","x":2.41,"y":-4.89},{"id":29,"label":"Gas Oil Hydrocarbon Results","size":20,"file":"GC Chromatography","color":"#e74c3c","x":2.82,"y":-3.48},{"id":30,"label":"Column Efficiency & Van Deemter","size":8,"file":"GC Chromatography","color":"#e74c3c","x":1.68,"y":-4.74},{"id":31,"label":"GCxGC Method Parameters","size":20,"file":"GC Chromatography","color":"#e74c3c","x":2.06,"y":-5.31},{"id":32,"label":"Chromatography Theory Fundamentals","size":8,"file":"GC Chromatography","color":"#e74c3c","x":1.46,"y":-4.14},{"id":33,"label":"Multidimensional GC Principles","size":14,"file":"GC Chromatography","color":"#e74c3c","x":1.32,"y":-3.63},{"id":34,"label":"GCxGC References & Literature","size":12,"file":"GC Chromatography","color":"#e74c3c","x":1.66,"y":-3.29},{"id":35,"label":"AGO & VGO Method Comparison","size":12,"file":"GC Chromatography","color":"#e74c3c","x":2.69,"y":-4.01},{"id":36,"label":"VGO Temperature Optimization","size":13,"file":"GC Chromatography","color":"#e74c3c","x":2.31,"y":-4.4}],"edges":[{"s":10,"t":11,"w":0.878,"type":"semantic"},{"s":10,"t":16,"w":0.844,"type":"semantic"},{"s":11,"t":16,"w":0.837,"type":"semantic"},{"s":13,"t":18,"w":0.85,"type":"semantic"},{"s":15,"t":16,"w":0.825,"type":"semantic"},{"s":16,"t":17,"w":0.833,"type":"semantic"},{"s":18,"t":23,"w":0.867,"type":"semantic"},{"s":19,"t":23,"w":0.839,"type":"semantic"},{"s":24,"t":25,"w":0.83,"type":"semantic"},{"s":24,"t":26,"w":0.831,"type":"semantic"},{"s":24,"t":27,"w":0.845,"type":"semantic"},{"s":26,"t":27,"w":0.872,"type":"semantic"},{"s":26,"t":29,"w":0.854,"type":"semantic"},{"s":27,"t":29,"w":0.852,"type":"semantic"},{"s":27,"t":34,"w":0.829,"type":"semantic"},{"s":27,"t":35,"w":0.827,"type":"semantic"},{"s":29,"t":35,"w":0.87,"type":"semantic"},{"s":31,"t":36,"w":0.86,"type":"semantic"},{"s":33,"t":34,"w":0.842,"type":"semantic"},{"s":35,"t":36,"w":0.908,"type":"semantic"},{"s":2,"t":6,"w":0.45,"type":"bridge","files":["GC Chromatography","Troms\u00f8 Seismic","Chalk Reservoir"]},{"s":6,"t":8,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":24,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":25,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":26,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":27,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":28,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":29,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":30,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":31,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":32,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":33,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":34,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":35,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":6,"t":36,"w":0.15,"type":"bridge","files":["GC Chromatography"]},{"s":2,"t":4,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":2,"t":7,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":2,"t":9,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":2,"t":13,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":2,"t":18,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":2,"t":19,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":2,"t":20,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":2,"t":21,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":2,"t":22,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":2,"t":23,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":4,"t":9,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":4,"t":13,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":4,"t":18,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":4,"t":19,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":4,"t":20,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":4,"t":21,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":4,"t":22,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":4,"t":23,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":7,"t":9,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":7,"t":13,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":7,"t":18,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":7,"t":19,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":7,"t":20,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":7,"t":21,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":7,"t":22,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":7,"t":23,"w":0.15,"type":"bridge","files":["Deepwater Drilling"]},{"s":0,"t":2,"w":0.15,"type":"bridge","files":["Gas Market"]},{"s":2,"t":3,"w":0.3,"type":"bridge","files":["Troms\u00f8 Seismic","Chalk Reservoir"]},{"s":3,"t":6,"w":0.3,"type":"bridge","files":["Troms\u00f8 Seismic","Chalk Reservoir"]},{"s":2,"t":5,"w":0.15,"type":"bridge","files":["Chalk Reservoir"]},{"s":3,"t":5,"w":0.15,"type":"bridge","files":["Chalk Reservoir"]},{"s":24,"t":25,"w":0.83,"type":"related"},{"s":24,"t":26,"w":0.831,"type":"related"},{"s":24,"t":27,"w":0.845,"type":"related"},{"s":24,"t":34,"w":0.807,"type":"related"},{"s":26,"t":27,"w":0.872,"type":"related"},{"s":26,"t":29,"w":0.854,"type":"related"},{"s":27,"t":29,"w":0.852,"type":"related"},{"s":27,"t":34,"w":0.829,"type":"related"},{"s":27,"t":35,"w":0.827,"type":"related"},{"s":29,"t":35,"w":0.87,"type":"related"},{"s":30,"t":36,"w":0.803,"type":"related"},{"s":31,"t":36,"w":0.86,"type":"related"},{"s":32,"t":33,"w":0.806,"type":"related"},{"s":33,"t":34,"w":0.842,"type":"related"},{"s":35,"t":36,"w":0.908,"type":"related"},{"s":9,"t":18,"w":0.813,"type":"related"},{"s":13,"t":18,"w":0.85,"type":"related"},{"s":13,"t":23,"w":0.8,"type":"related"},{"s":18,"t":23,"w":0.867,"type":"related"},{"s":19,"t":22,"w":0.807,"type":"related"},{"s":19,"t":23,"w":0.839,"type":"related"},{"s":1,"t":11,"w":0.819,"type":"related"},{"s":10,"t":11,"w":0.878,"type":"related"},{"s":10,"t":12,"w":0.812,"type":"related"},{"s":10,"t":16,"w":0.844,"type":"related"},{"s":11,"t":16,"w":0.837,"type":"related"},{"s":14,"t":15,"w":0.801,"type":"related"},{"s":15,"t":16,"w":0.825,"type":"related"},{"s":16,"t":17,"w":0.833,"type":"related"}],"colors":{"GC Chromatography":"#e74c3c","Gas Market":"#3498db","Chalk Reservoir":"#2ecc71","Deepwater Drilling":"#f39c12","Kick & Loss Lab":"#9b59b6","Troms\u00f8 Seismic":"#1abc9c"}};
const N = DATA.nodes, E = DATA.edges;
const STOP = new Set('a an the of in for to and is it what how which are from by on with this that do does can where when'.split(' '));
const SAMPLES = ['gas property of north sea reservoir','deepwater riser problems','chromatography sulphur analysis','hydrate formation drilling','kick detection MPD','seismic interpretation','oil composition vacuum gas','well control barrier'];
// Build sample buttons
const samplesEl = document.getElementById('samples');
SAMPLES.forEach(s => {
const b = document.createElement('button');
b.textContent = s;
b.onclick = () => { document.getElementById('qinput').value = s; runQuery(); };
samplesEl.appendChild(b);
});
// Enter key
document.getElementById('qinput').addEventListener('keydown', e => { if(e.key==='Enter') runQuery(); });
// Force layout
const SN = N.map(n => {
const jitter = () => (Math.random()-.5)*5;
return {...n, sx: n.x*18+jitter(), sy: n.y*18+jitter(), vx:0, vy:0, r: Math.sqrt(n.size)*2.2+5};
});
for(let it=0;it<350;it++){
for(let i=0;i<SN.length;i++) for(let j=i+1;j<SN.length;j++){
let dx=SN[j].sx-SN[i].sx, dy=SN[j].sy-SN[i].sy, d=Math.sqrt(dx*dx+dy*dy)||1;
let f=700/(d*d), fx=dx/d*f, fy=dy/d*f;
SN[i].vx-=fx; SN[i].vy-=fy; SN[j].vx+=fx; SN[j].vy+=fy;
}
for(const e of E){
const a=SN.find(n=>n.id===e.s), b=SN.find(n=>n.id===e.t);
if(!a||!b) continue;
let dx=b.sx-a.sx, dy=b.sy-a.sy, d=Math.sqrt(dx*dx+dy*dy)||1;
let str = e.type==='semantic'?.07 : e.type==='related'?.035 : .008;
let f=(d-75)*str, fx=dx/d*f, fy=dy/d*f;
a.vx+=fx; a.vy+=fy; b.vx-=fx; b.vy-=fy;
}
for(const n of SN){ n.vx*=.84; n.vy*=.84; n.sx+=n.vx; n.sy+=n.vy; }
}
const cxm=SN.reduce((s,n)=>s+n.sx,0)/SN.length, cym=SN.reduce((s,n)=>s+n.sy,0)/SN.length;
SN.forEach(n=>{n.sx-=cxm; n.sy-=cym;});
// Canvas
const canvas=document.getElementById('c'), ctx=canvas.getContext('2d'), tip=document.getElementById('tip');
let W,H,dpr;
let panX=0,panY=0,zoom=1,isPan=false,panSX,panSY,panSPX,panSPY;
let hovered=null, selected=null;
// Query state
let matchedSet=new Set(), activeSet=new Set(), activeEdges=new Set();
function resize(){
dpr=window.devicePixelRatio||1;
const r=canvas.getBoundingClientRect();
W=r.width; H=r.height;
canvas.width=W*dpr; canvas.height=H*dpr;
ctx.scale(dpr,dpr);
}
resize(); window.addEventListener('resize',()=>{resize();draw()});
function draw(){
ctx.clearRect(0,0,W,H);
ctx.save();
ctx.translate(W/2+panX, H/2+panY);
ctx.scale(zoom,zoom);
const sc=Math.min(W,H)/850;
const hasQuery = matchedSet.size > 0;
// Edges
for(let i=0;i<E.length;i++){
const e=E[i];
const a=SN.find(n=>n.id===e.s), b=SN.find(n=>n.id===e.t);
if(!a||!b) continue;
const eKey = e.s+'-'+e.t;
const isActive = activeEdges.has(eKey);
let col = e.type==='semantic'?'#5eead4' : e.type==='bridge'?'#f59e0b' : '#6366f1';
let alpha, width;
if(hasQuery){
if(isActive){ alpha=.8; width=2.5; } else { alpha=.03; width=.3; }
} else if(selected!==null){
if(e.s===selected||e.t===selected){ alpha=.7; width=2; } else { alpha=.04; width=.3; }
} else { alpha=.15; width=.6; }
ctx.beginPath();
ctx.moveTo(a.sx*sc, a.sy*sc);
ctx.lineTo(b.sx*sc, b.sy*sc);
ctx.strokeStyle=col; ctx.globalAlpha=alpha; ctx.lineWidth=width;
ctx.stroke();
}
// Nodes
for(const n of SN){
const isMatch = matchedSet.has(n.id);
const isTraversed = activeSet.has(n.id);
let r=n.r*sc*.45, alpha;
if(hasQuery){
if(isMatch){ alpha=1; r*=1.4; } else if(isTraversed){ alpha=.8; r*=1.1; } else { alpha=.08; }
} else if(selected!==null){
const conn = E.some(e=>(e.s===selected&&e.t===n.id)||(e.t===selected&&e.s===n.id));
if(n.id===selected){ alpha=1; r*=1.3; } else if(conn){ alpha=.8; } else { alpha=.12; }
} else { alpha=.85; }
if(hovered===n.id){ r*=1.15; alpha=Math.max(alpha,.9); }
// Glow for matched
if(isMatch){
ctx.globalAlpha=.25;
ctx.beginPath(); ctx.arc(n.sx*sc, n.sy*sc, r*2.2, 0, Math.PI*2);
ctx.fillStyle=n.color; ctx.fill();
}
// Node circle
ctx.globalAlpha=alpha;
ctx.beginPath(); ctx.arc(n.sx*sc, n.sy*sc, r, 0, Math.PI*2);
ctx.fillStyle=n.color; ctx.fill();
if(isMatch){
ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.stroke();
}
// Label
const showLabel = (!hasQuery && (r>5||hovered===n.id||selected===n.id))
|| (hasQuery && (isMatch||isTraversed));
if(showLabel){
ctx.globalAlpha = isMatch ? 1 : (hasQuery ? alpha*.9 : alpha*.85);
const fs = isMatch ? Math.max(10, r*.9) : Math.max(7, Math.min(10, r*.7));
ctx.font = (isMatch?'600 ':'') + fs + "px 'DM Sans',sans-serif";
ctx.fillStyle = isMatch ? '#fff' : '#c8d1dc';
ctx.textAlign='center';
ctx.fillText(n.label.substring(0,28), n.sx*sc, n.sy*sc + r + fs + 2);
if(isMatch){
ctx.font = (fs-2)+"px 'JetBrains Mono',monospace";
ctx.fillStyle='#64748b';
ctx.fillText(n.file+' · '+n.size+' chunks', n.sx*sc, n.sy*sc + r + fs*2 + 2);
}
}
}
ctx.globalAlpha=1;
ctx.restore();
}
// Query logic
function extractKW(q){ return q.toLowerCase().split(/\s+/).filter(w=>w.length>1&&!STOP.has(w)); }
function findMatches(kws){
return N.map(n=>{
const ll=n.label.toLowerCase(), fl=n.file.toLowerCase();
let score=0; const matched=[];
for(const k of kws) if(ll.includes(k)||fl.includes(k)){score++;matched.push(k);}
return {...n, score, matched};
}).filter(n=>n.score>0).sort((a,b)=>b.score-a.score||b.size-a.size);
}
function doTraversal(startIds, maxHops){
const visited=new Set(startIds); let current=new Set(startIds);
const results=[], edgeKeys=new Set();
for(let hop=1;hop<=maxHops;hop++){
const next=new Set();
for(const nid of current){
for(const e of E){
let nb=null;
if(e.s===nid&&!visited.has(e.t)) nb=e.t;
if(e.t===nid&&!visited.has(e.s)) nb=e.s;
if(nb!==null){
visited.add(nb); next.add(nb);
const nd=N.find(n=>n.id===nb);
results.push({...nd,hop,edgeType:e.type,weight:e.w});
edgeKeys.add(e.s+'-'+e.t);
}
}
}
// Also capture edges between already-visited nodes
for(const e of E){
if(visited.has(e.s)&&visited.has(e.t)) edgeKeys.add(e.s+'-'+e.t);
}
current=next;
}
return {results, edgeKeys, visited};
}
function runQuery(){
const q = document.getElementById('qinput').value.trim();
const res = document.getElementById('results');
if(!q){ clearQuery(); return; }
const kws = extractKW(q);
if(!kws.length){ res.innerHTML='<div class="no-query">No meaningful keywords found</div>'; matchedSet.clear(); activeSet.clear(); activeEdges.clear(); draw(); return; }
const matches = findMatches(kws);
const topIds = matches.slice(0,5).map(m=>m.id);
const cypher1 = `MATCH (t:Topic)\nWHERE ${kws.map(k=>`toLower(t.label) CONTAINS '${k}'`).join('\n OR ')}\nRETURN t.label, t.chunk_count, t.source_document\nORDER BY t.chunk_count DESC LIMIT 5`;
const cypher2 = `MATCH (start:Topic)-[r*1..2]-(neighbor:Topic)\nWHERE start.cluster_id IN [${topIds.join(', ')}]\nRETURN DISTINCT neighbor.label,\n neighbor.source_document,\n min(length(r)) AS hops\nORDER BY hops ASC LIMIT 15`;
const trav = topIds.length>0 ? doTraversal(topIds, 2) : null;
// Update highlight state
matchedSet = new Set(topIds);
activeSet = trav ? trav.visited : new Set(topIds);
activeEdges = trav ? trav.edgeKeys : new Set();
// Build results HTML
let html = '';
// Keywords
html += '<div class="step-title">KEYWORDS</div>';
html += kws.map(k=>`<span class="kw">${k}</span>`).join(' ');
// Matches
html += `<div class="step-title">MATCHED NODES (${matches.length} hits)</div>`;
matches.slice(0,5).forEach(m=>{
html += `<div class="match-card" style="border-left-color:${m.color}">
<div style="width:9px;height:9px;border-radius:50%;background:${m.color};flex-shrink:0"></div>
<div style="flex:1"><div class="mc-label">${m.label}</div>
<div class="mc-meta">${m.file} · ${m.size} chunks · matched: [${m.matched.join(', ')}]</div></div>
<div class="mc-score">${m.score}</div></div>`;
});
// Traversal results
if(trav && trav.results.length>0){
html += `<div class="step-title">MATCHED HOPS (${trav.results.length} connected)</div>`;
[1,2].forEach(hop=>{
const hr = trav.results.filter(r=>r.hop===hop);
if(!hr.length) return;
html += `<div style="font-size:10px;color:#64748b;font-family:monospace;margin:6px 0 3px">${'→'.repeat(hop)} Hop ${hop} (${hr.length} nodes):</div>`;
hr.slice(0,10).forEach(r=>{
const ec = r.edgeType==='semantic'?'etype-semantic':r.edgeType==='bridge'?'etype-bridge':'etype-related';
html += `<div class="hop-item" style="margin-left:${hop*12}px">
<div class="dot" style="background:${r.color}"></div>
<span style="color:#cbd5e1">${r.label}</span>
<span class="etype ${ec}">${r.edgeType}</span>
<span style="color:#475569;font-size:9px">${r.file}</span></div>`;
});
});
}
res.innerHTML = html;
draw();
}
function clearQuery(){
matchedSet.clear(); activeSet.clear(); activeEdges.clear();
document.getElementById('results').innerHTML='<div class="no-query">Type a query or click a sample above</div>';
selected=null;
draw();
}
function esc(s){ return s.replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// Mouse interaction on canvas
canvas.addEventListener('mousemove', e=>{
const rect=canvas.getBoundingClientRect();
const mx=e.clientX-rect.left, my=e.clientY-rect.top;
if(isPan){panX=panSPX+(e.clientX-panSX);panY=panSPY+(e.clientY-panSY);draw();return;}
const sc=Math.min(W,H)/850;
const cx=W/2+panX, cy=H/2+panY;
let found=null;
for(const n of SN){
const nx=cx+n.sx*sc*zoom, ny=cy+n.sy*sc*zoom, r=n.r*sc*.45*zoom;
if(Math.hypot(mx-nx,my-ny)<r+5){found=n.id;break;}
}
if(found!==hovered){
hovered=found;
canvas.style.cursor=found!==null?'pointer':'grab';
if(found!==null){
const nd=N.find(n=>n.id===found);
const ec=E.filter(e=>e.s===found||e.t===found).length;
tip.innerHTML=`<div class="tt-l" style="color:${nd.color}">${nd.label}</div><div class="tt-m">${nd.file} · ${nd.size} chunks · ${ec} edges</div>`;
tip.style.left=(mx+14)+'px';tip.style.top=(my-8)+'px';tip.style.opacity=1;
} else { tip.style.opacity=0; }
draw();
} else if(found!==null){ tip.style.left=(mx+14)+'px';tip.style.top=(my-8)+'px'; }
});
canvas.addEventListener('mousedown',e=>{
if(hovered!==null && matchedSet.size===0){
selected=selected===hovered?null:hovered; draw();
} else { isPan=true;panSX=e.clientX;panSY=e.clientY;panSPX=panX;panSPY=panY; }
});
canvas.addEventListener('mouseup',()=>{isPan=false});
canvas.addEventListener('wheel',e=>{
e.preventDefault();zoom*=e.deltaY>0?.92:1.08;zoom=Math.max(.2,Math.min(5,zoom));draw();
},{passive:false});
draw();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment