|
<html lang="en"> |
|
|
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Demo</title> |
|
|
|
<script src="https://d3js.org/d3.v5.min.js"></script> |
|
|
|
<script src="hover.js"></script> |
|
<script src="helpers.js"></script> |
|
|
|
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,900" rel="stylesheet" type="text/css"> |
|
<link rel="stylesheet" href="style.css"> |
|
</head> |
|
|
|
<body> |
|
|
|
<h2>Coauthorship in Network Science</h2> |
|
|
|
<svg></svg> |
|
|
|
<p> |
|
<em>*Only showing the second largest connected component.</em> |
|
<br/> |
|
<b>Data:</b> M. E. J. Newman, Finding Community Structure in Networks using the Eigenvectors of Matrices, Preprint Physics/0605087 (2006). |
|
[<a href="http://www-personal.umich.edu/~mejn/netdata/">Data</a>] |
|
[<a href="https://arxiv.org/abs/physics/0605087">Paper</a>] |
|
</p> |
|
|
|
<script> |
|
let graph = null; |
|
|
|
const w = 700; // svg width |
|
const h = 700; // svg height |
|
const r = 6; // node radius |
|
|
|
// calculate fixed radial amount |
|
const radial = (Math.min(w, h) / 2) - (2 * r); |
|
|
|
const scales = { |
|
color: d3.scaleSequential(d3.interpolateYlGnBu), |
|
theta: d3.scaleLinear().range([0, 2 * Math.PI]) |
|
}; |
|
|
|
const svg = d3.select('svg') |
|
.attr('width', w) |
|
.attr('height', h); |
|
|
|
const g = { |
|
plot: svg.append('g').attr('id', 'plot') |
|
}; |
|
|
|
// shift plot so (0, 0) is in center |
|
g.plot.attr('transform', `translate(${w / 2}, ${h / 2})`); |
|
|
|
// place links underneath nodes |
|
g.links = g.plot.append('g').attr('id', 'links'); |
|
g.nodes = g.plot.append('g').attr('id', 'nodes'); |
|
|
|
// craft url where datafile is located |
|
const base = 'https://gist.githubusercontent.com'; |
|
const user = 'sjengle'; |
|
const gist = '510ffd96ad06fc3921aee0425e962c1e'; |
|
const file = 'netscience-second.json'; |
|
const path = [base, user, gist, 'raw', file].join('/'); |
|
|
|
d3.json(path).then(callback); |
|
|
|
function callback(data) { |
|
// save data globally for debugging |
|
graph = data; |
|
console.log(file, 'loaded:', graph); |
|
|
|
// setup color scale using closeness |
|
const closeness = d3.extent(graph.nodes, v => v.closeness) |
|
scales.color.domain(closeness); |
|
|
|
// calculate node layout, break up 360° for the nodes |
|
scales.theta.domain([0, graph.nodes.length]); |
|
|
|
g.control = g.plot.append('g') |
|
.attr('id', 'control') |
|
.style('pointer-events', 'none'); |
|
|
|
// make graph links easier to work with by replacing |
|
// node indices with node objects |
|
graph.links.forEach(function(e, i) { |
|
e.source = isNaN(e.source) ? e.source : graph.nodes[e.source]; |
|
e.target = isNaN(e.target) ? e.target : graph.nodes[e.target]; |
|
}); |
|
|
|
// now we can safely sort the nodes without causing issues with links |
|
graph.nodes.sort((a, b) => d3.ascending(a.closeness, b.closeness)); |
|
|
|
// and next we can calculate node positions |
|
graph.nodes.forEach(function(v, i) { |
|
v.radial = radial; |
|
v.theta = scales.theta(i); |
|
|
|
const coords = toCartesian(v.radial, v.theta); |
|
v.x = coords.x; |
|
v.y = coords.y; |
|
}); |
|
|
|
// draw nodes |
|
const nodes = g.nodes.selectAll('circle.node') |
|
.data(graph.nodes) |
|
.enter() |
|
.append('circle') |
|
.attr('class', 'node') |
|
.attr('r', r) |
|
.attr('cx', v => v.x) |
|
.attr('cy', v => v.y) |
|
.style('fill', v => scales.color(v.closeness)); |
|
|
|
const bundle = generateSegments(graph); |
|
console.log('bundle:', bundle); |
|
|
|
// draw control nodes for reference |
|
const control = g.control.selectAll('circle.control') |
|
.data(bundle.nodes) |
|
.enter() |
|
.append('circle') |
|
.attr('class', 'control') |
|
.attr('r', 2) |
|
.attr('cx', c => c.x) |
|
.attr('cy', c => c.y) |
|
.style('fill', '#252525') |
|
.style('stroke', 'silver') |
|
.style('pointer-events', 'none'); |
|
|
|
// want line segments drawn smoothly |
|
// use line generator with basis interpolation |
|
const line = d3.line() |
|
.curve(d3.curveCardinal) |
|
.x(v => v.x) |
|
.y(v => v.y); |
|
|
|
// draw edges |
|
const links = g.links.selectAll('path.link') |
|
.data(bundle.paths) |
|
.enter() |
|
.append('path') |
|
.attr('d', line) |
|
.attr('class', 'link') |
|
.style('stroke-width', 2) |
|
.style('stroke', e => scales.color(d3.mean([e[0].closeness, e[e.length - 1].closeness]))); |
|
|
|
// setup node tooltips |
|
setupTooltip(nodes); |
|
setupHighlight(nodes, links); |
|
|
|
const layout = d3.forceSimulation() |
|
.force('collide', d3.forceCollide(2)) |
|
.force('charge', d3.forceManyBody().strength(0.3)) |
|
.force('link', d3.forceLink().strength(0.5).distance(0)); |
|
|
|
layout.on('tick', function(v) { |
|
control.attr('cx', v => v.x); |
|
control.attr('cy', v => v.y); |
|
|
|
links.attr('d', line); |
|
}) |
|
.on('end', function(v) { |
|
control.remove(); |
|
}); |
|
|
|
layout.nodes(bundle.nodes); |
|
layout.force('link').links(bundle.links); |
|
} |
|
|
|
/* |
|
* Turns a single edge into several segments that can |
|
* be used for simple edge bundling. |
|
*/ |
|
function generateSegments(graph) { |
|
|
|
// number of inner nodes depends on how far nodes are apart |
|
const inner = d3.scaleLinear() |
|
.domain([0, radial * 2]) |
|
.range([3, 30]); |
|
|
|
// generate separate graph for edge bundling |
|
// nodes: all nodes including control nodes |
|
// links: all individual links (source to target) |
|
// paths: all segments combined into single path for drawing |
|
const bundle = {nodes: [], links: [], paths: []}; |
|
|
|
// make existing nodes fixed |
|
bundle.nodes = graph.nodes.map(function(d, i) { |
|
d.fx = d.x; |
|
d.fy = d.y; |
|
return d; |
|
}); |
|
|
|
graph.links.forEach(function(d, i) { |
|
// fix graph links to map to objects instead of indices |
|
d.source = isNaN(d.source) ? d.source : graph.nodes[d.source]; |
|
d.target = isNaN(d.target) ? d.target : graph.nodes[d.target]; |
|
|
|
// calculate the distance between the source and target |
|
const length = distance(d.source, d.target); |
|
|
|
// calculate total number of inner nodes for this link |
|
const total = Math.round(inner(length)); |
|
|
|
// create scales from source to target |
|
const xscale = d3.scaleLinear() |
|
.domain([0, total + 1]) // source, inner nodes, target |
|
.range([d.source.x, d.target.x]); |
|
|
|
const yscale = d3.scaleLinear() |
|
.domain([0, total + 1]) |
|
.range([d.source.y, d.target.y]); |
|
|
|
// initialize source node |
|
let source = d.source; |
|
let target = null; |
|
|
|
// add all points to local path |
|
let local = [source]; |
|
|
|
for (let j = 1; j <= total; j++) { |
|
// calculate target node |
|
target = { |
|
x: xscale(j), |
|
y: yscale(j) |
|
}; |
|
|
|
local.push(target); |
|
bundle.nodes.push(target); |
|
|
|
bundle.links.push({ |
|
source: source, |
|
target: target |
|
}); |
|
|
|
source = target; |
|
} |
|
|
|
local.push(d.target); |
|
|
|
// add last link to target node |
|
bundle.links.push({ |
|
source: target, |
|
target: d.target |
|
}); |
|
|
|
bundle.paths.push(local); |
|
}); |
|
|
|
return bundle; |
|
} |
|
|
|
</script> |
|
</body> |