const screenWidth = document.documentElement.clientWidth;
const screenHeight = document.documentElement.clientHeight;
const pxRatio = window.devicePixelRatio;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const w = canvas.width = screenWidth * devicePixelRatio;
const h = canvas.height = screenHeight * devicePixelRatio;
const stats = {
queries: 0,
totalTime: 0,
getAverage() {
return `${this.totalTime / this.queries}ms`;
},
clear() {
this.totalTime = this.queries = 0;
}
};
canvas.style.width = screenWidth + 'px';
canvas.style.height = screenHeight + 'px';
function scaleGraphToFitBounds(G, bounds) {
var min = Math.min, max = Math.max;
var bbox = getBounds(G);
var cx = (bounds[0] + bounds[2]) / 2,
cy = (bounds[1] + bounds[3]) / 2,
gcx = (bbox[0] + bbox[2]) / 2,
gcy = (bbox[1] + bbox[3]) / 2;
var tx = cx - gcx,
ty = cy - gcy;
var scale = min(
(bounds[2] - bounds[0]) / (bbox[2] - bbox[0]),
(bounds[3] - bounds[1]) / (bbox[3] - bbox[1])
);
G.nodes.forEach(function (n) {
n.x = cx + ((n.x + tx) - cx) * scale;
n.y = cy + ((n.y + ty) - cy) * scale;
});
}
function getBounds(g) {
return g.nodes.reduce((b, n) => {
b[0] = Math.min(b[0], n.x);
b[1] = Math.min(b[1], n.y);
b[2] = Math.max(b[2], n.x);
b[3] = Math.max(b[3], n.y);
return b;
}, [Infinity, Infinity, -Infinity, -Infinity]);
}
function generateDenseTree(N = 550, width = 1000, height = 1000) {
let counter = 0;
return {
nodes: new Array(N).fill(0).map(function(_, i) {
counter++;
return {
id: i,
text: 'Node #' + i,
x: Math.random() * width,
y: Math.random() * height,
r: 3 + Math.random() * 8
};
}),
edges: new Array(N - 1).fill(0).map(function(_, i) {
counter++;
return {
id: i,
curvature: (i % 3 === 0) ? 1 : 1,
//curvature: 0,
source: Math.floor(Math.sqrt(i)),
target: i + 1
};
})
};
}
var N = 1000;
var data, nodesMap, simulation;
const SZ = 5;
var flatStorage;
let foundNodes = [];
let foundEdges = [];
let selectedNode = null;
let showIndex = false;
function populate(n) {
N = n;
foundNodes = [];
foundEdges = [];
selectedNode = null;
if (simulation) simulation.stop();
data = generateDenseTree(N, w, h);
nodesMap = data.nodes.reduce((a, n) => {
a[n.id] = n;
return a;
}, {});
flatStorage = new Float32Array(SZ * 2 * N);
simulation = layout(data, onLayoutDone);
}
const qBounds = [0, 0, 0, 0];
function render() {
ctx.clearRect(0, 0, w, h);
if (showIndex) {
ctx.lineWidth = 0.5;
renderIndex();
ctx.lineWidth = 1;
ctx.beginPath();
U._checkedNodes.forEach((n) => {
const b = U.keyToBBox(n.key);
ctx.rect(b[0], b[1], b[2] - b[0], b[3] - b[1]);
});
ctx.globalAlpha = 0.2;
ctx.fillStyle = 'blue';
ctx.fill();
ctx.globalAlpha = 1;
}
const Nn = data.nodes.length;
// const { nodes, edges } = U.size === 0 ? data : U.query([0, 0, w, h])
// .reduce((a, id) => {
// if (!a.ids[id]) {
// a.ids[id] = true;
// if (id >= Nn) a.edges.push(data.edges[id - Nn]);
// else a.nodes.push(data.nodes[id]);
// }
// return a;
// }, { nodes: [], edges: [], ids: {} });
const { nodes, edges } = data;
// edges
ctx.beginPath();
ctx.strokeStyle = '#707070';
edges.forEach((e) => {
const s = nodesMap[e.source],
t = nodesMap[e.target];
ctx.moveTo(s.x, s.y);
if (e.curvature === 0) {
ctx.lineTo(t.x, t.y);
} else {
const cp = getQuadraticControlPoint(s.x, s.y, t.x, t.y);
ctx.quadraticCurveTo(cp.x, cp.y, t.x, t.y);
}
});
ctx.stroke();
ctx.closePath();
// highlighted edges
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.strokeStyle = '#505050';
ctx.lineWidth = 5;
foundEdges.forEach((e) => {
const s = nodesMap[e.source],
t = nodesMap[e.target];
ctx.moveTo(s.x, s.y);
if (e.curvature === 0) {
ctx.lineTo(t.x, t.y);
} else {
const cp = getQuadraticControlPoint(s.x, s.y, t.x, t.y);
ctx.quadraticCurveTo(cp.x, cp.y, t.x, t.y);
}
});
ctx.stroke();
// nodes;
ctx.beginPath();
ctx.fillStyle = 'orange';
ctx.strokeStyle = '#333333';
ctx.lineWidth = 1;
nodes.forEach((n) => {
ctx.moveTo(n.x + n.r, n.y);
ctx.arc(n.x, n.y, n.r, 0, 2 * Math.PI, false);
});
ctx.stroke();
ctx.fill();
if (selectedNode) {
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.strokeStyle = '#333333';
ctx.moveTo(selectedNode.x + selectedNode.r, selectedNode.y);
ctx.arc(selectedNode.x, selectedNode.y, selectedNode.r, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.fill();
}
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.strokeStyle = '#333333';
ctx.lineWidth = 15;
foundNodes.forEach((n) => {
ctx.moveTo(n.x + n.r, n.y);
ctx.arc(n.x, n.y, n.r, 0, 2 * Math.PI, false);
});
ctx.stroke();
ctx.fill();
ctx.lineWidth = 1;
// query
ctx.beginPath();
ctx.strokeStyle = 'brown';
ctx.rect(qBounds[0], qBounds[1], qBounds[2] - qBounds[0], qBounds[3] - qBounds[1]);
ctx.closePath();
ctx.stroke();
}
function renderIndex() {
const map = {};
const nodesToDraw = [];
U.forEach((n) => {
if (!map[n.key]) {
map[n.key] = true;
nodesToDraw.push(n);
}
});
ctx.beginPath();
nodesToDraw.forEach((n) => {
const b = U.keyToBBox(n.key);
ctx.rect(b[0], b[1], b[2] - b[0], b[3] - b[1]);
});
ctx.closePath();
ctx.stroke();
}
function getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
canvas.addEventListener('mousemove', ({ clientX, clientY }) => {
const x = clientX * pxRatio;
const y = clientY * pxRatio;
const offset = 10 * pxRatio;
foundNodes.length = 0;
foundEdges.length = 0;
qBounds[0] = x - offset;
qBounds[1] = y - offset;
qBounds[2] = x + offset;
qBounds[3] = y + offset;
const Nn = data.nodes.length;
const before = Date.now();
U.query(qBounds).forEach((id) => {
if (id >= Nn) {
foundEdges.push(data.edges[id - Nn]);
} else {
foundNodes.push(data.nodes[id]);
}
});
stats.totalTime += (Date.now() - before);
stats.queries++;
requestAnimationFrame(render);
});
canvas.addEventListener('click', ({ clientX, clientY }) => {
const mx = clientX * pxRatio;
const my = clientY * pxRatio;
requestAnimationFrame(render);
});
function onLayoutDone() {
indexNodes();
indexEdges();
}
function indexNodes() {
console.time('insert nodes into UB');
data.nodes.forEach(({ x, y, r }, i) => {
const bbox = [x - r, y - r, x + r, y + r];
const pos = i * SZ;
flatStorage[pos + 0] = bbox[0];
flatStorage[pos + 1] = bbox[1];
flatStorage[pos + 2] = bbox[2];
flatStorage[pos + 3] = bbox[3];
flatStorage[pos + 4] = POLYGON;
U.insert(i);
});
console.timeEnd('insert nodes into UB');
}
function indexEdges() {
console.time('insert edges into UB');
const offset = data.nodes.length;
data.edges.forEach(({ source, target, curvature }, i) => {
const s = nodesMap[source],
t = nodesMap[target];
const pos = SZ * (offset + i);
// console.log(source, target);
flatStorage[pos + 0] = /*s.x; */ source;
flatStorage[pos + 1] = /*s.y; */ target;
flatStorage[pos + 2] = curvature; // t.x;
flatStorage[pos + 3] = t.y;
flatStorage[pos + 4] = curvature === 0 ? LINESTRING : CURVE;
U.insert(offset + i);
});
console.timeEnd('insert edges into UB');
}
function updateNodes() {
data.nodes.map((n, i) => {
const { x, y, r } = n;
const pos = i * SZ;
flatStorage[pos + 0] = x - r;
flatStorage[pos + 1] = y - r;
flatStorage[pos + 2] = x + r;
flatStorage[pos + 3] = y + r;
flatStorage[pos + 4] = POLYGON;
U.update(i);
});
}
function updateEdges() {
const offset = data.nodes.length;
data.edges.forEach(({ source, target }, i) => {
const s = nodesMap[source],
t = nodesMap[target];
const pos = SZ * (offset + i);
flatStorage[pos + 0] = source; //s.x;
flatStorage[pos + 1] = target; //s.y;
flatStorage[pos + 2] = t.x;
flatStorage[pos + 3] = t.y;
flatStorage[pos + 4] = LINESTRING;
U.update(offset + i);
});
}
function clearNodesIndex() {
console.time('clear nodes');
data.nodes.forEach((e, i) => U.remove(i))
console.timeEnd('clear nodes');
}
function clearEdgesIndex() {
console.time('clear edges');
const offset = data.nodes.length;
data.edges.forEach((e, i) => U.remove(offset + i));
console.timeEnd('clear edges');
}
function layout(data, callback = () => {}) {
const m = data.nodes.length < 1000 ? 1.2 : 1;
return d3.forceSimulation(data.nodes)
.force('charge', d3.forceManyBody())
.force('link', d3.forceLink(data.edges.map((e) => ({
id: e.id,
target: e.target,
source: e.source
}))
)
.id((d) => d.id)
.distance(25 * m))
.force('collision', d3.forceCollide(15 * m))
.force('center', d3.forceCenter(w / 2, h / 2))
.force("y", d3.forceY(0))
.force("x", d3.forceX(0))
.alphaDecay(0.1)
.on('tick', render)
.on('end', callback);
}
const U = new UBTree(6);
const form = document.querySelector('#control');
form.addEventListener('change', function() {
setTimeout(function() {
var nodesIndex = form['nodes'].checked;
var edgesIndex = form['edges'].checked;
var level = form['level'].value;
U.clear();
U.setLevel(level);
if (nodesIndex) indexNodes();
if (edgesIndex) indexEdges();
showIndex = form['grid'].checked;
render();
}, 50);
});
const indicator = document.querySelector('#indicator');
const rangeInput = document.querySelector('#range');
var populateTimer = 0;
function onGraphChanged() {
clearTimeout(populateTimer);
populateTimer = requestAnimationFrame(() => {
N = parseInt(rangeInput.value);
indicator.innerHTML = `G = (${N}V × ${N}E)`;
populate(N);
});
}
rangeInput.addEventListener('change', onGraphChanged);
onGraphChanged();