This node-link diagram shows the Twitter users who used the #EuroVis hashtag during EuroVis 2019 (thanks to John Alexis Guerra Gómez for collecting the data!). If a user retweeted, replied, or quoted another user’s tweet, there is a link connecting them. Nodes with darker colors had more #EuroVis tweets.
This example shows how to combine the Random Vertex Sampling algorithm from d3.forceManyBodySampled()
with the Barnes-Hut algorithm from d3.forceManyBody()
. The example first computes a fast layout using Random Vertex sampling, and then runs 10 itereations of the Barnes-Hut algorithm to refine the layout.
More information about the algorithm is available in the blog post.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
background: white;
font: 12px sans-serif;
}
.d3-tip strong {
color: #fafafa;
}
.d3-tip {
line-height: 1;
font-weight: normal;
padding: 8px;
background: rgba(0, 0, 0, 0.8);
color: #eee;
border-radius: 2px;
pointer-events: none !important;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
position: absolute;
pointer-events: none;
}
/* Northward tooltips */
.d3-tip.n:after {
content: "\25BC";
margin: -1px 0 0 0;
top: 100%;
left: 0;
text-align: center;
}
.links line {
stroke: #999;
stroke-opacity: 0.4;
}
.nodes circle {
stroke: #333;
stroke-width: 2px;
}
</style>
<svg></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="d3-tip.js"></script>
<script src="d3-force-sampled.js"></script>
<script>
var width = 960;
var height = 600;
var nodeRadius = 6;
var nodeColor = d3.scaleSequential().interpolator(d3.interpolateMagma);
var linkWidth = d3.scaleLinear().range([1, 2 * nodeRadius]);
var svg = d3.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'scale(0.5)');
var nodeTip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return '<strong>'
+ d.screen_name
+ '</strong> had <strong>'
+ d.tweet_count
+ (d.tweet_count === 1 ? '</strong> tweet' : '</strong> tweets');
});
var linkTip = d3.tip()
.attr('class', 'd3-tip')
.offset(function() {
return [this.getBBox().height / 4 - 10, 0];
})
.html(function(d) {
return '<p style="width:200px;"><strong>'
+ d.source.screen_name
+ '</strong> and <strong>'
+ d.target.screen_name
+ '</strong> retweeted, replied, or quoted each other <strong>'
+ d.interact_count
+ (d.interact_count === 1 ? '</strong> time' : '</strong> times')
+ '</p>';
});
svg.call(nodeTip);
svg.call(linkTip);
var linkForce = d3.forceLink()
.id(function(d) { return d.id; });
var forceSim = d3.forceSimulation()
.velocityDecay(0.2)
.force('link', linkForce)
.force('charge', d3.forceManyBodySampled())
.force('forceX', d3.forceX().strength(0.015))
.force('forceY', d3.forceY().strength(0.015 * width / height))
.force('center', d3.forceCenter(width, height));
d3.json('Eurovis2019_rt_network.json').then(function (graph) {
var drag = d3.drag()
.on('drag', dragging);
var nodes = graph.nodes.reduce(function (userMap, d) {
var user;
if (userMap.has(d.user.screen_name)) {
user = userMap.get(d.user.screen_name);
++user.tweet_count;
user.retweet_count += d.retweet_count;
user.favorite_count += d.favorite_count;
} else {
user = {
favorite_count: d.favorite_count,
retweet_count: d.retweet_count,
screen_name: d.user.screen_name,
tweet_count: 1
};
userMap.set(user.screen_name, user);
}
return userMap;
}, d3.map());
var tweetsToUsers = graph.nodes.reduce(function (tweetMap, d) {
tweetMap.set(d.id, d.user.screen_name);
return tweetMap;
}, d3.map());
var links = graph.links.reduce(function (interactMap, d) {
var link;
var sourceUser = nodes.get(tweetsToUsers.get(+d.source));
var targetUser = nodes.get(tweetsToUsers.get(+d.target));
if (sourceUser && targetUser && sourceUser !== targetUser) {
var link;
if (interactMap.has(sourceUser.screen_name + '-' + targetUser.screen_name))
++interactMap.get(sourceUser.screen_name + '-' + targetUser.screen_name).interact_count;
else if (interactMap.has(targetUser.screen_name + '-' + sourceUser.screen_name))
++interactMap.get(targetUser.screen_name + '-' + sourceUser.screen_name).interact_count;
else
interactMap.set(sourceUser.screen_name + '-' + targetUser.screen_name, {source: sourceUser, target: targetUser, interact_count: 1});
}
return interactMap;
}, d3.map());
nodes = nodes.values();
links = links.values();
// Make sure small nodes are drawn on top of larger nodes
nodes.sort(function (a, b) { return b.tweet_count - a.tweet_count; });
nodeColor.domain([d3.max(nodes, function (d) { return d.tweet_count; }), 0]);
linkWidth.domain([0, d3.max(links, function (d) { return d.interact_count; })]);
var link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter().append('line')
.attr('stroke-width', function (d) { return linkWidth(d.interact_count); })
.on('mouseover', linkTip.show)
.on('mouseout', linkTip.hide);
var node = svg.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(nodes)
.enter().append('circle')
.attr('r', nodeRadius)
.attr('fill', function (d) { return nodeColor(d.tweet_count); })
.call(drag)
.on('mouseover', nodeTip.show)
.on('mouseout', nodeTip.hide);
forceSim.nodes(nodes)
.on('tick', draw)
.stop();
forceSim.force('link').links(links);
for (var t = 100; t > 0; --t) forceSim.tick();
forceSim.velocityDecay(0.4)
.force('charge', d3.forceManyBody());
for (var t = 10; t > 0; --t) forceSim.tick();
draw();
function draw () {
link
.attr('x1', function (d) { return d.source.x; })
.attr('x2', function (d) { return d.target.x; })
.attr('y1', function (d) { return d.source.y; })
.attr('y2', function (d) { return d.target.y; });
node
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
}
function dragging (d) {
d.x = d3.event.x;
d.y = d3.event.y;
draw();
nodeTip.hide();
}
});
</script>
// Copyright 2019 Two Six Labs, LLC. v1.0.0 d3-force-sampled https://github.com/twosixlabs/d3-force-sampled/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.d3 = global.d3 || {})));
}(this, (function (exports) { 'use strict';
function constant(x) {
return function() {
return x;
};
}
function manyBodySampled() {
var nodes,
alpha,
strength = constant(-30),
strengths,
indicesRepulse,
prevIndex = 0,
distanceMin2 = 1,
distanceMax2 = Infinity,
neighborSize = function () {
return 15;
},
updateSize = function (nodes) { return Math.pow(nodes.length, 0.75); },
sampleSize = function (nodes) { return Math.pow(nodes.length, 0.25); },
numNeighbors,
numUpdate,
numSamples,
chargeMultiplier = function (nodes) {
return nodes.length < 100 ? 1 : nodes.length < 200 ? 3 : Math.sqrt(nodes.length);
},
cMult,
rand = Math.random;
function addRandomNode(node) {
var randIdx = Math.floor(rand() * nodes.length),
randNode = nodes[randIdx],
randDist = (node.x - randNode.x) * (node.x - randNode.x) + (node.y - randNode.y) * (node.y - randNode.y),
currIdx,
currNode,
currDist,
maxI,
maxDist = -Infinity,
i = -1;
// Is this already in the list?
if (node.nearest.indexOf(randIdx) >= 0) return;
// If there is room for another, add it.
if (node.nearest.length < numNeighbors) {
node.nearest.push(randIdx);
return;
}
// Replace the farthest away "neighbor" with the new node.
while (++i < node.nearest.length) {
currIdx = node.nearest[i];
currNode = nodes[currIdx];
currDist = Math.hypot(node.x - currNode.x, node.y - currNode.y);
if (currDist > maxDist) {
maxI = i;
maxDist = currDist;
}
}
if (randDist < maxDist) {
node.nearest[maxI] = randIdx;
}
}
function getRandIndices(indices, num) {
num = Math.floor(num);
var i,
n = nodes.length,
cnt = n - num,
randIdx,
temp;
// Choose random indices.
for (i = n-1; i >= cnt; --i) {
randIdx = Math.floor(rand() * i);
temp = indices[randIdx];
indices[randIdx] = indices[i];
indices[i] = temp;
}
return indices.slice(cnt);
}
function approxRepulse(node) {
var i,
randIndices,
currNode,
w,
x,
y,
l;
// Choose random nodes to update.
randIndices = getRandIndices(indicesRepulse, numSamples);
for (i = randIndices.length - 1; i >= 0; --i) {
currNode = nodes[randIndices[i]];
if (currNode === node) continue;
x = currNode.x - node.x;
y = currNode.y - node.y;
l = x * x + y * y;
if (l >= distanceMax2) continue;
// Limit forces for very close nodes; randomize direction if coincident.
if (x === 0) x = (rand() - 0.5) * 1e-6, l += x * x;
if (y === 0) y = (rand() - 0.5) * 1e-6, l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
w = strengths[node.index] * alpha * cMult / l;
node.vx += x * w;
node.vy += y * w;
}
}
function constantRepulse(node) {
var i,
nearest,
currNode,
w,
x,
y,
l;
// Update the list of nearest nodes.
if (numNeighbors) addRandomNode(node);
nearest = node.nearest;
if (numNeighbors) for (i = nearest.length - 1; i >= 0; --i) {
currNode = nodes[nearest[i]];
if (currNode === node) continue;
x = currNode.x - node.x;
y = currNode.y - node.y;
l = x * x + y * y;
if (l >= distanceMax2) continue;
// Limit forces for very close nodes; randomize direction if coincident.
if (x === 0) x = (rand() - 0.5) * 1e-6, l += x * x;
if (y === 0) y = (rand() - 0.5) * 1e-6, l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
w = strengths[node.index] * alpha * cMult / l;
node.vx += x * w;
node.vy += y * w;
}
}
function force(_) {
var i = 0, j = prevIndex, n = nodes.length, upperIndex = prevIndex + numUpdate;
for (alpha = _; i < n || j < upperIndex; ++i, ++j) {
if (j < upperIndex) approxRepulse(nodes[j%n]);
if (numNeighbors && i < n) constantRepulse(nodes[i]);
}
prevIndex = upperIndex % n;
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
indicesRepulse = new Array(n);
for (i = 0; i < n; ++i) indicesRepulse[i] = i;
strengths = new Array(n);
// Cannot be negative.
numNeighbors = Math.min(Math.ceil(neighborSize(nodes)), n);
numNeighbors = numNeighbors < 0 ? 0 : Math.min(numNeighbors, nodes.length);
numUpdate = Math.ceil(updateSize(nodes));
numUpdate = numUpdate < 0 ? 0 : Math.min(numUpdate, n);
numSamples = Math.ceil(sampleSize(nodes));
numSamples = numSamples < 0 ? 0 : Math.min(numSamples, n);
cMult = chargeMultiplier(nodes);
alpha = 1;
for (i = 0; i < n; ++i) {
node = nodes[i];
strengths[node.index] = +strength(node, i, nodes);
node.nearest = [];
while (node.nearest.length < numNeighbors) addRandomNode(node);
}
}
force.initialize = function(_) {
nodes = _;
initialize();
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};
force.distanceMin = function(_) {
return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2);
};
force.distanceMax = function(_) {
return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2);
};
force.neighborSize = function(_) {
return arguments.length ? (neighborSize = typeof _ === "function" ? _ : constant(+_), initialize(), force) : neighborSize;
};
force.updateSize = function(_) {
return arguments.length ? (updateSize = typeof _ === "function" ? _ : constant(+_), initialize(), force) : updateSize;
};
force.sampleSize = function(_) {
return arguments.length ? (sampleSize = typeof _ === "function" ? _ : constant(+_), initialize(), force) : sampleSize;
};
force.chargeMultiplier = function(_) {
return arguments.length ? (chargeMultiplier = typeof _ === "function" ? _ : constant(+_), initialize(), force) : chargeMultiplier;
};
force.source = function(_) {
return arguments.length ? (rand = _, force) : rand;
};
return force;
}
exports.forceManyBodySampled = manyBodySampled;
Object.defineProperty(exports, '__esModule', { value: true });
})));
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('d3-collection'), require('d3-selection')) :
typeof define === 'function' && define.amd ? define(['d3-collection', 'd3-selection'], factory) :
(global.d3 = global.d3 || {}, global.d3.tip = factory(global.d3,global.d3));
}(this, (function (d3Collection,d3Selection) { 'use strict';
/**
* d3.tip
* Copyright (c) 2013-2017 Justin Palmer
*
* Tooltips for d3.js SVG visualizations
*/
// Public - constructs a new tooltip
//
// Returns a tip
function index() {
var direction = d3TipDirection,
offset = d3TipOffset,
html = d3TipHTML,
rootElement = document.body,
node = initNode(),
svg = null,
point = null,
target = null;
function tip(vis) {
svg = getSVGNode(vis);
if (!svg) return
point = svg.createSVGPoint();
rootElement.appendChild(node);
}
// Public - show the tooltip on the screen
//
// Returns a tip
tip.show = function() {
var args = Array.prototype.slice.call(arguments);
if (args[args.length - 1] instanceof SVGElement) target = args.pop();
var content = html.apply(this, args),
poffset = offset.apply(this, args),
dir = direction.apply(this, args),
nodel = getNodeEl(),
i = directions.length,
coords,
scrollTop = document.documentElement.scrollTop ||
rootElement.scrollTop,
scrollLeft = document.documentElement.scrollLeft ||
rootElement.scrollLeft;
nodel.html(content)
.style('opacity', 1).style('pointer-events', 'all');
while (i--) nodel.classed(directions[i], false);
coords = directionCallbacks.get(dir).apply(this);
nodel.classed(dir, true)
.style('top', (coords.top + poffset[0]) + scrollTop + 'px')
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px');
return tip
};
// Public - hide the tooltip
//
// Returns a tip
tip.hide = function() {
var nodel = getNodeEl();
nodel.style('opacity', 0).style('pointer-events', 'none');
return tip
};
// Public: Proxy attr calls to the d3 tip container.
// Sets or gets attribute value.
//
// n - name of the attribute
// v - value of the attribute
//
// Returns tip or attribute value
// eslint-disable-next-line no-unused-vars
tip.attr = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().attr(n)
}
var args = Array.prototype.slice.call(arguments);
d3Selection.selection.prototype.attr.apply(getNodeEl(), args);
return tip
};
// Public: Proxy style calls to the d3 tip container.
// Sets or gets a style value.
//
// n - name of the property
// v - value of the property
//
// Returns tip or style property value
// eslint-disable-next-line no-unused-vars
tip.style = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().style(n)
}
var args = Array.prototype.slice.call(arguments);
d3Selection.selection.prototype.style.apply(getNodeEl(), args);
return tip
};
// Public: Set or get the direction of the tooltip
//
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
// sw(southwest), ne(northeast) or se(southeast)
//
// Returns tip or direction
tip.direction = function(v) {
if (!arguments.length) return direction
direction = v == null ? v : functor(v);
return tip
};
// Public: Sets or gets the offset of the tip
//
// v - Array of [x, y] offset
//
// Returns offset or
tip.offset = function(v) {
if (!arguments.length) return offset
offset = v == null ? v : functor(v);
return tip
};
// Public: sets or gets the html value of the tooltip
//
// v - String value of the tip
//
// Returns html value or tip
tip.html = function(v) {
if (!arguments.length) return html
html = v == null ? v : functor(v);
return tip
};
// Public: sets or gets the root element anchor of the tooltip
//
// v - root element of the tooltip
//
// Returns root node of tip
tip.rootElement = function(v) {
if (!arguments.length) return rootElement
rootElement = v == null ? v : functor(v);
return tip
};
// Public: destroys the tooltip and removes it from the DOM
//
// Returns a tip
tip.destroy = function() {
if (node) {
getNodeEl().remove();
node = null;
}
return tip
};
function d3TipDirection() { return 'n' }
function d3TipOffset() { return [0, 0] }
function d3TipHTML() { return ' ' }
var directionCallbacks = d3Collection.map({
n: directionNorth,
s: directionSouth,
e: directionEast,
w: directionWest,
nw: directionNorthWest,
ne: directionNorthEast,
sw: directionSouthWest,
se: directionSouthEast
}),
directions = directionCallbacks.keys();
function directionNorth() {
var bbox = getScreenBBox(this);
return {
top: bbox.n.y - node.offsetHeight,
left: bbox.n.x - node.offsetWidth / 2
}
}
function directionSouth() {
var bbox = getScreenBBox(this);
return {
top: bbox.s.y,
left: bbox.s.x - node.offsetWidth / 2
}
}
function directionEast() {
var bbox = getScreenBBox(this);
return {
top: bbox.e.y - node.offsetHeight / 2,
left: bbox.e.x
}
}
function directionWest() {
var bbox = getScreenBBox(this);
return {
top: bbox.w.y - node.offsetHeight / 2,
left: bbox.w.x - node.offsetWidth
}
}
function directionNorthWest() {
var bbox = getScreenBBox(this);
return {
top: bbox.nw.y - node.offsetHeight,
left: bbox.nw.x - node.offsetWidth
}
}
function directionNorthEast() {
var bbox = getScreenBBox(this);
return {
top: bbox.ne.y - node.offsetHeight,
left: bbox.ne.x
}
}
function directionSouthWest() {
var bbox = getScreenBBox(this);
return {
top: bbox.sw.y,
left: bbox.sw.x - node.offsetWidth
}
}
function directionSouthEast() {
var bbox = getScreenBBox(this);
return {
top: bbox.se.y,
left: bbox.se.x
}
}
function initNode() {
var div = d3Selection.select(document.createElement('div'));
div
.style('position', 'absolute')
.style('top', 0)
.style('opacity', 0)
.style('pointer-events', 'none')
.style('box-sizing', 'border-box');
return div.node()
}
function getSVGNode(element) {
var svgNode = element.node();
if (!svgNode) return null
if (svgNode.tagName.toLowerCase() === 'svg') return svgNode
return svgNode.ownerSVGElement
}
function getNodeEl() {
if (node == null) {
node = initNode();
// re-add node to DOM
rootElement.appendChild(node);
}
return d3Selection.select(node)
}
// Private - gets the screen coordinates of a shape
//
// Given a shape on the screen, will return an SVGPoint for the directions
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast),
// nw(northwest), sw(southwest).
//
// +-+-+
// | |
// + +
// | |
// +-+-+
//
// Returns an Object {n, s, e, w, nw, sw, ne, se}
function getScreenBBox(targetShape) {
var targetel = target || targetShape;
while (targetel.getScreenCTM == null && targetel.parentNode != null) {
targetel = targetel.parentNode;
}
var bbox = {},
matrix = targetel.getScreenCTM(),
tbbox = targetel.getBBox(),
width = tbbox.width,
height = tbbox.height,
x = tbbox.x,
y = tbbox.y;
point.x = x;
point.y = y;
bbox.nw = point.matrixTransform(matrix);
point.x += width;
bbox.ne = point.matrixTransform(matrix);
point.y += height;
bbox.se = point.matrixTransform(matrix);
point.x -= width;
bbox.sw = point.matrixTransform(matrix);
point.y -= height / 2;
bbox.w = point.matrixTransform(matrix);
point.x += width;
bbox.e = point.matrixTransform(matrix);
point.x -= width / 2;
point.y -= height / 2;
bbox.n = point.matrixTransform(matrix);
point.y += height;
bbox.s = point.matrixTransform(matrix);
return bbox
}
// Private - replace D3JS 3.X d3.functor() function
function functor(v) {
return typeof v === 'function' ? v : function() {
return v
}
}
return tip
}
return index;
})));