const screenWidth = document.documentElement.clientWidth;
const screenHeight = document.documentElement.clientHeight;
const pxRatio = window.devicePixelRatio;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.style.width = screenWidth + 'px';
canvas.style.height = screenHeight + 'px';
// global width/height to use for rendering, accunting for retina screens
const w = canvas.width = screenWidth * pxRatio;
const h = canvas.height = screenHeight * pxRatio;
let data = { nodes: [], edges: [] }; // store graph here
let nodesMap = {};
// store highlighted nodes and edges there to render
const highlightedEdges = [];
const highlightedNodes = [];
// central node to be highlighted differently, use as you want
let selectedNode = null;
let q = d3.quadtree([], n => n.x, n => n.y);
let transform = { x: 0, y: 0, k: 1 };
function render() {
if (d3.event) transform = d3.event.transform;
ctx.save();
ctx.clearRect(0, 0, w, h);
ctx.translate(transform.x * pxRatio, transform.y * pxRatio);
ctx.scale(transform.k, transform.k);
const Nn = data.nodes.length;
const { nodes, edges } = data;
// edges
ctx.lineWidth = 3;
ctx.beginPath();
ctx.strokeStyle = '#707070';
edges.forEach((e) => {
const s = nodesMap[e.source],
t = nodesMap[e.target];
ctx.moveTo(s.x, s.y);
ctx.lineTo(t.x, t.y);
});
ctx.stroke();
ctx.closePath();
// highlighted edges
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.strokeStyle = '#505050';
ctx.lineWidth = 5;
highlightedEdges.forEach((e) => {
const s = nodesMap[e.source],
t = nodesMap[e.target];
ctx.moveTo(s.x, s.y);
ctx.lineTo(t.x, t.y);
});
ctx.closePath();
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();
// selected node
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();
}
// highlighted nodes
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.strokeStyle = '#333333';
ctx.lineWidth = 15;
highlightedNodes.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();
drawQuadTree(ctx);
ctx.restore();
}
function drawQuadTree(ctx) {
q = d3.quadtree([], n => n.x, n => n.y);
q.addAll(data.nodes);
q.visit((n) => {
if (n.length) {
for (let i = 0; i < 4; i++) if (n[i]) n[i].parent = n;
}
});
q.visitAfter((quad) => {
var strength = 0, q, c, weight = 0, x, y, i;
const out = [];
// For internal nodes, accumulate forces from child quadrants.
if (quad.length) {
const ins = [];
for (x = y = i = 0; i < 4; ++i) {
if ((q = quad[i]) && q.med) ins.push(q.med);
}
out.push(quad.med = d3.packEnclose(ins));
}
// For leaf nodes, accumulate forces from coincident quadrants.
else {
out.push(quad.med = quad.data);
// do strength += strengths[q.data.index]; while (q = q.next);
}
quad.med = d3.packEnclose(out);
});
ctx.beginPath();
ctx.globalAlpha = 0.75;
ctx.lineWidth = 0.5;
q.visit((n, x0, y0, x1, y1) => {
ctx.rect(x0, y0, x1 - x0, y1 - y0);
if (n.length) {
ctx.moveTo(n.med.x + n.med.r, n.med.y);
ctx.arc(n.med.x, n.med.y, n.med.r, 0, 2 * Math.PI, false);
}
});
ctx.stroke();
ctx.globalAlpha = 1;
ctx.closePath();
}
d3.select('canvas')
.call(d3.zoom().scaleExtent([0.1, 10]).on('zoom', function() {
transform = d3.event.transform;
requestAnimationFrame(render);
}));
// render on mouse move
canvas.addEventListener('mousemove', ({ clientX, clientY }) => {
const x = clientX * pxRatio;
const y = clientY * pxRatio;
const offset = 10 * pxRatio;
// qBounds[0] = x - offset;
// qBounds[1] = y - offset;
// qBounds[2] = x + offset;
// qBounds[3] = y + offset;
requestAnimationFrame(render);
});
const form = document.querySelector('#params');
function onChange() {
// read controls, apply actions
console.log(form['input1'].value);
console.log(form['input2'].value);
}
// listen to form changes
form.addEventListener('change', onChange);
// graph size control
const indicator = document.querySelector('#indicator');
const rangeInput = document.querySelector('#range');
let populateTimer = 0;
let N = 550;
function onGraphChanged() {
clearTimeout(populateTimer);
populateTimer = requestAnimationFrame(() => {
N = parseInt(rangeInput.value);
indicator.innerHTML = `G = (${N}V × ${N}E)`;
init(N);
});
}
rangeInput.addEventListener('change', onGraphChanged);
// center view;
document.querySelector('#center-view').addEventListener('click', () => {
const padding = 20 * pxRatio;
scaleGraphToFitBounds(data, [
0 + padding, 0 + padding,
w - padding, h - padding
]);
});
function init (N) {
onChange();
data = generateTree(N);
nodesMap = data.nodes.reduce((a, n) => {
a[n.id] = n;
return a;
}, {});
layout(data, render).then(render);
}
onGraphChanged();