Created
April 6, 2026 16:03
-
-
Save yohanesnuwara/ac5a92055656fa35bf3386ad0aa71420 to your computer and use it in GitHub Desktop.
Geo-Documents Knowledge Graph
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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,'<').replace(/>/g,'>'); } | |
| // 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