Spatial query lens
This is the JavaScript used by the first playground preset. Edit and run it in the interactive playground, including its HTML and CSS.
import {RBush,Envelope} from '@wieslawsoltes/rbushweb';
const count=2000,tree=new RBush(9),canvas=document.querySelector('#spatial'),ctx=canvas.getContext('2d');
let seed=19;const random=()=>((seed=(seed*1664525+1013904223)>>>0)/4294967296);
const items=Array.from({length:count},(_,id)=>{const x=random()*900,y=random()*440;return{id,x,y,Envelope:new Envelope(x,y,x+3,y+3)};});
tree.BulkLoad(items);let x=450,y=220;
function draw(){
const r=Number(document.querySelector('#radius').value),near=document.querySelector('#mode').value==='nearest';
const start=performance.now(),found=near?tree.Knn(20,x,y):tree.Search(new Envelope(x-r,y-r,x+r,y+r));
const elapsed=performance.now()-start,selected=new Set(found);
ctx.fillStyle=document.documentElement.dataset.theme==='dark'?'#14171c':'#f6f7f9';ctx.fillRect(0,0,900,440);
for(const item of items){ctx.fillStyle=selected.has(item)?'#a7d260':'#606a78';ctx.fillRect(item.x,item.y,selected.has(item)?5:2,selected.has(item)?5:2);}
ctx.strokeStyle='#b9e477';ctx.lineWidth=1;
if(!near){ctx.fillStyle='#a7d26015';ctx.fillRect(x-r,y-r,r*2,r*2);ctx.strokeRect(x-r,y-r,r*2,r*2);}
else {for(const p of found){ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(p.x,p.y);ctx.stroke();}}
ctx.beginPath();ctx.arc(x,y,5,0,Math.PI*2);ctx.stroke();
document.querySelector('#result').textContent=found.length+' / '+count.toLocaleString()+' · '+elapsed.toFixed(2)+' ms query';
}
canvas.onpointermove=e=>{const b=canvas.getBoundingClientRect();x=(e.clientX-b.left)*900/b.width;y=(e.clientY-b.top)*440/b.height;draw();};
canvas.onkeydown=e=>{if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key))return;e.preventDefault();x=Math.max(0,Math.min(900,x+(e.key==='ArrowRight'?12:e.key==='ArrowLeft'?-12:0)));y=Math.max(0,Math.min(440,y+(e.key==='ArrowDown'?12:e.key==='ArrowUp'?-12:0)));draw();};
document.querySelector('#center').onclick=()=>{x=450;y=220;draw();};
document.querySelector('#radius').oninput=draw;document.querySelector('#mode').onchange=draw;
draw();console.log('Indexed',tree.Count,'synthetic points. Query time excludes drawing.');Show the HTML host
<div class="toolbar"><label>Query <select id="mode"><option value="area">Rectangle intersection</option><option value="nearest">20 nearest neighbors</option></select></label><label>Radius <input id="radius" type="range" min="20" max="140" value="75"></label><button id="center">Center query</button><output id="result"></output></div><canvas id="spatial" width="900" height="440" tabindex="0" aria-label="Spatial query canvas. Use arrow keys to move the query." style="width:100%;height:auto;border:1px solid var(--line);border-radius:10px"></canvas><p class="hint">Move the pointer, touch the canvas, or focus it and use arrow keys. Results come from RBush.Search or RBush.Knn.</p>How it works
Give geometry an index
Index immutable envelopes, query the relevant area, and keep rendering separate. The same headless engine works in the browser and workers.
Create an RBush, then Insert or BulkLoad objects with an Envelope. Search with a rectangle or use Knn for nearest neighbors.
Own the lifetime
Delete an item using its old bounds before moving it, then insert it with the new envelope. Clear an index when its dataset is discarded.
The playground replaces its entire isolated frame on each run. In your application, use the framework’s unmount or disposal hook instead. Do not dispose a shared model while another view still owns it.
Read the lifecycle guide →Know the contract
Source repositories can contain work newer than the published package. The showcase uses @wieslawsoltes/rbushweb@0.1.1.