|
/* global d3 */ |
|
|
|
d3.json('graph.json', (error, graph) => { |
|
const nodes = graph.nodes; |
|
console.log('nodes', nodes); |
|
|
|
const margin = { top: 100, right: 100, bottom: 100, left: 100 }; |
|
|
|
const width = 960; |
|
const height = 500; |
|
|
|
// separation between same-color circles |
|
const padding = 1.5; |
|
|
|
// separation between different-color circles |
|
const clusterPadding = 6; |
|
|
|
const maxRadius = 12; |
|
|
|
const z = d3.scaleOrdinal(d3.schemeCategory20); |
|
|
|
// total number of nodes |
|
const n = nodes.length; |
|
|
|
// collect clusters from nodes |
|
const clusters = {}; |
|
nodes.forEach((node) => { |
|
const radius = node.r; |
|
const clusterID = node.cluster; |
|
if (!clusters[clusterID] || (radius > clusters[clusterID].r)) { |
|
clusters[clusterID] = node; |
|
} |
|
}); |
|
console.log('clusters', clusters); |
|
|
|
const svg = d3.select('body') |
|
.append('svg') |
|
.attr('height', height) |
|
.attr('width', width) |
|
.append('g') |
|
.attr('transform', `translate(${width / 2},${height / 2})`); |
|
|
|
const circles = svg.append('g') |
|
.datum(nodes) |
|
.selectAll('.circle') |
|
.data(d => d) |
|
.enter().append('circle') |
|
.attr('r', d => d.r) |
|
.attr('fill', d => z(d.cluster)) |
|
.attr('stroke', 'black') |
|
.attr('stroke-width', 1); |
|
|
|
const simulation = d3.forceSimulation(nodes) |
|
.velocityDecay(0.2) |
|
.force('x', d3.forceX().strength(0.0005)) |
|
.force('y', d3.forceY().strength(0.0005)) |
|
.force('collide', collide) |
|
.force('cluster', clustering) |
|
.on('tick', ticked); |
|
|
|
function ticked() { |
|
circles |
|
.attr('cx', d => d.x) |
|
.attr('cy', d => d.y); |
|
} |
|
|
|
// These are implementations of the custom forces |
|
function clustering(alpha) { |
|
nodes.forEach((d) => { |
|
const cluster = clusters[d.cluster]; |
|
if (cluster === d) return; |
|
let x = d.x - cluster.x; |
|
let y = d.y - cluster.y; |
|
let l = Math.sqrt((x * x) + (y * y)); |
|
const r = d.r + cluster.r; |
|
if (l !== r) { |
|
l = ((l - r) / l) * alpha; |
|
d.x -= x *= l; |
|
d.y -= y *= l; |
|
cluster.x += x; |
|
cluster.y += y; |
|
} |
|
}); |
|
} |
|
|
|
function collide(alpha) { |
|
const quadtree = d3.quadtree() |
|
.x(d => d.x) |
|
.y(d => d.y) |
|
.addAll(nodes); |
|
|
|
nodes.forEach((d) => { |
|
const r = d.r + maxRadius + Math.max(padding, clusterPadding); |
|
const nx1 = d.x - r; |
|
const nx2 = d.x + r; |
|
const ny1 = d.y - r; |
|
const ny2 = d.y + r; |
|
quadtree.visit((quad, x1, y1, x2, y2) => { |
|
if (quad.data && (quad.data !== d)) { |
|
let x = d.x - quad.data.x; |
|
let y = d.y - quad.data.y; |
|
let l = Math.sqrt((x * x) + (y * y)); |
|
const r = d.r + quad.data.r + (d.cluster === quad.data.cluster ? padding : clusterPadding); |
|
if (l < r) { |
|
l = ((l - r) / l) * alpha; |
|
d.x -= x *= l; |
|
d.y -= y *= l; |
|
quad.data.x += x; |
|
quad.data.y += y; |
|
} |
|
} |
|
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1; |
|
}); |
|
}); |
|
} |
|
}); |