This node-link diagram shows the Twitter users who used the #EuroVis hashtag during EuroVis 2019. 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.
forked from rpgove's block: EuroVis 2019 Twitter interaction network
xxxxxxxxxx
<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>
https://d3js.org/d3.v5.min.js