const h = document.documentElement.clientHeight; const w = document.documentElement.clientWidth; var canvas = d3.select('canvas').node(); var ctx = canvas.getContext('2d'); var candidatesInput = document.getElementById('candidates'); var minRadiusInput = document.getElementById('minRadius'); var pxRatio = window.devicePixelRatio; var width = w * pxRatio; var height = h * pxRatio; var nodeSize = 5; var counter = 0; canvas.width = width; canvas.height = height; canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; var color; function generateDenseTree(N = 550) { 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: Math.random() * 30 }; }), edges: new Array(N - 1).fill(0).map(function(_, i) { counter++; return { id: i, source: Math.floor(Math.sqrt(i)), target: i + 1 }; }) }; } var nodes, edges, adjacencyTable, groups, layers, visibility; var graphId; var simulation; function setGraph(graph, noLayout) { nodes = graph.nodes; edges = graph.edges; idMap = nodes.reduce((map, node) => { map[node.id] = node; return map; }, {}); adjacencyTable = edges.reduce((acc, edge) => { var { source, target } = edge; acc[source] = acc[source] || []; if (acc[source].indexOf(target) === -1)acc[source].push(target); acc[target] = acc[target] || []; if (acc[target].indexOf(source) === -1) acc[target].push(source); return acc; }, {}); nodes.forEach((n) => { if (!adjacencyTable[n.id]) adjacencyTable[n.id] = []; n.degree = adjacencyTable[n.id].length; if (n.attributes) { n.x = n.attributes.x; n.y = n.attributes.y; } }); scaleGraphToFitBounds({ nodes, edges }, [0, 0, width, height]); if (simulation) simulation.stop(); if (noLayout) { redraw(); } else { simulation = d3.forceSimulation(nodes) .force('charge', d3.forceManyBody()) .force('link', d3.forceLink(edges.map((e) => ({ id: e.id, target: e.target, source: e.source })) ) .id((d) => d.id) .distance(50)) .force('collision', d3.forceCollide(25)) .force('center', d3.forceCenter(width / 2, height / 2)) .force("y", d3.forceY(0)) .force("x", d3.forceX(0)) .on('tick', redraw) .on('end', () => { console.log('done'); simulation = null; }); } } function onChange() { var type = document.getElementById('graph-select').value; switch (type) { case 'er500x2000': d3.json('er500x2000.json', (err, graph) => { if (!err) setGraph(graph); }); break; case 'tree-1500': setGraph(generateDenseTree(1500)); break; case 'tree-550': // fall through default: setGraph(generateDenseTree(550)); break; } } function onLayerRangeSelect() { const lrs = Object.keys(groups); const value = lrs.length - this.value; lrs.reverse().forEach((l) => layers[l] = parseInt(l) >= value); nodes.forEach((n) => visibility[n.id] = layers[n.layer]); redraw(); } document.getElementById('graph-select').addEventListener('change', onChange); onChange(); function overlaps (a, b) { return (a[3] >= b[1] && a[1] <= b[3]) && (a[2] >= b[0] && a[0] <= b[2]); } function query(Q, aabb, map) { const result = []; Q.visit((qnode, qx0, qy0, qx1, qy1) => { const qaabb = [qx0, qy0, qx1, qy1]; const node = qnode.data; if (node && map[node.id]) { const { x, y, r } = node; const naabb = [x - r, y - r, x + r, y + r]; if (overlaps(aabb, naabb)) result.push(node); } return !overlaps(aabb, qaabb); }); return result; } function distance(n1, n2) { const dx = n1.x - n2.x, dy = n1.y - n2.y; return Math.sqrt(dx * dx + dy * dy); } function scan (dr = 5) { const unclustered = nodes.reduce((m, n) => { m[n.id] = n; return m; }, {}); const Q = d3.quadtree() .x(n => n.x) .y(n => n.y) .addAll(nodes); const clusters = []; nodes.forEach((n) => { if (unclustered[n.id]) { // all intersecting nodes const halobbox = [ n.x - n.r - dr, n.y - n.r - dr, n.x + n.r + dr, n.y + n.r + dr ]; const intersects = query(Q, halobbox, unclustered); if (intersects.length !== 0) { let found = 0; let cx = n.x, cy = n.y; let dist, maxDist = 0; const bbox = intersects.reduce((box, node) => { if (node.id !== n.id && unclustered[node.id] && (dist = distance(node, n)) < (n.r + dr + node.r + dr)) { const { x, y, r } = node; box[0] = Math.min(x - r - dr, box[0]); box[1] = Math.min(y - r - dr, box[1]); box[2] = Math.max(x + r + dr, box[2]); box[3] = Math.max(y + r + dr, box[3]); cx += x; cy += y; delete unclustered[node.id]; found++; } return box; }, halobbox); if (found) { cx /= (found + 1); cy /= (found + 1); // const cx = (bbox[0] + bbox[2]) / 2; // const cy = (bbox[1] + bbox[3]) / 2; // const r = Math.max((bbox[2] - bbox[0]) / 2, (bbox[3] - bbox[1]) / 2); const r = Math.max( Math.abs(bbox[0] - cx), Math.abs(bbox[2] - cx), Math.abs(bbox[1] - cy), Math.abs(bbox[3] - cy) ); delete unclustered[n.id]; clusters.push({ x: cx, y: cy, r, bbox }); } } } }); return { clusters, unclustered }; } function redraw() { const haloDiff = 20; ctx.clearRect(0, 0, width, height); // clustered halos const { clusters, unclustered } = scan(haloDiff); ctx.beginPath(); clusters.forEach(cluster => drawNode(cluster)); ctx.fillStyle = '#888888'; ctx.fill(); // ctx.strokeStyle = '#222222'; // ctx.stroke(); // halos ctx.beginPath(); nodes.forEach((node) => { if (unclustered[node.id]) { drawNode({ x: node.x, y: node.y, r: node.r + haloDiff }); } }); ctx.fillStyle = '#888888'; ctx.fill(); // ctx.strokeStyle = '#222222'; // ctx.stroke(); // edges ctx.beginPath(); edges.forEach(e => drawEdge(e)); ctx.strokeStyle = 'rgba(0,0,0,0.8)'; ctx.lineWidth = 2; ctx.stroke(); // clustered nodes ctx.beginPath(); nodes.forEach((node) => { if (!unclustered[node.id]) drawNode(node); }); ctx.fillStyle = '#4286f4'; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.stroke(); // unclustered nodes ctx.beginPath(); nodes.forEach((node) => { if (unclustered[node.id]) drawNode(node); }); ctx.fillStyle = '#28f5fc'; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.stroke(); // ctx.beginPath(); // clusters.forEach((c) => { // const bbox = c.bbox; // ctx.moveTo(bbox[0], bbox[1]); // ctx.rect(bbox[0], bbox[1], bbox[2] - bbox[0], bbox[3] - bbox[1]); // }); // ctx.strokeStyle = 'red'; // ctx.stroke(); } function drawNode(site) { ctx.moveTo(site.x + site.r, site.y); ctx.arc(site.x, site.y, site.r, 0, 2 * Math.PI, false); } function drawCircle({ radius, center, closest }) { var [cx, cy] = center; ctx.fillStyle = closest ? 'rgba(100,0,0,0.1)' : 'rgba(0,100,0,0.1)'; ctx.moveTo(cx, cy); ctx.beginPath(); ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false); ctx.fill(); ctx.stroke(); } function drawEdge(edge) { var source = nodes[edge.source], target = nodes[edge.target]; ctx.moveTo(source.x, source.y); ctx.lineTo(target.x, target.y); } 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]); }