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();