Relationship between twitter users during Openvis Conf. Lines indicate mentions, from source to target in clockwise fashion.
Built with blockbuilder.org
forked from sxywu's block: openvis tweets #1
Relationship between twitter users during Openvis Conf. Lines indicate mentions, from source to target in clockwise fashion.
Built with blockbuilder.org
forked from sxywu's block: openvis tweets #1
| <!DOCTYPE html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.10.0/d3-legend.js"></script> | |
| <script src='https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.11.2/lodash.js'></script> | |
| <link href='https://fonts.googleapis.com/css?family=Lora' rel='stylesheet' type='text/css'> | |
| <style> | |
| body { | |
| font-family: 'Lora', serif; | |
| margin:0; | |
| } | |
| svg { | |
| width: 900px; | |
| height: 600px; | |
| } | |
| text { | |
| font-size: .8em; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <svg></svg> | |
| <script> | |
| d3.json('https://raw.githubusercontent.com/sxywu/fishy/master/clean-results_min.json', function(response) { | |
| var radius = 40; | |
| var force = d3.layout.force() | |
| .size([900, 600]) | |
| .linkDistance(175) | |
| .charge(function(d) {return -d.length * 10}) | |
| .on('tick', updatePositions); | |
| var tweets = {}; | |
| var users = _.chain(response.results) | |
| .filter(function(t) { | |
| return !t.body.match(/^RT/); | |
| }).groupBy(function(t) { | |
| t.date = new Date(t.postedTime); | |
| // save the tweets by their links | |
| tweets[t.link] = t; | |
| return t.actor.preferredUsername | |
| }).sortBy(function(t, username) { | |
| return -t.length; | |
| }).reduce(function(obj, t) { | |
| var username = t[0].actor.preferredUsername; | |
| obj[username] = { | |
| tweets: t, | |
| username: username, | |
| length: t.length | |
| }; | |
| return obj; | |
| }, {}).value(); | |
| var links = {}; | |
| _.each(tweets, function(tweet) { | |
| // go through each tweet and make links | |
| // based on mentions | |
| var sourceUser = tweet.actor.preferredUsername; | |
| _.each(tweet.twitter_entities.user_mentions, function(mention) { | |
| var targetUser = mention.screen_name; | |
| // if (!users[targetUser]) { | |
| // users[targetUser] = { | |
| // tweets: [], | |
| // username: targetUser, | |
| // length: 0 | |
| // }; | |
| // } | |
| if (users[targetUser]) { | |
| var link = links[sourceUser + ',' + targetUser]; | |
| if (!link) { | |
| link = links[sourceUser + ',' + targetUser] = { | |
| source: users[sourceUser], | |
| target: users[targetUser], | |
| count: 0 | |
| }; | |
| } | |
| link.count += 1; | |
| } | |
| }); | |
| }); | |
| var usersArray = _.values(users); | |
| var linksArray = _.values(links); | |
| // calculate the radius for each user | |
| var minRadius = d3.min(usersArray, function(d) {return d.length}); | |
| var maxRadius = d3.max(usersArray, function(d) {return d.length}); | |
| var radiusScale = d3.scale.linear() | |
| .domain([minRadius, maxRadius]) | |
| .range([radius / 10, radius]); | |
| var svg = d3.select('svg'); | |
| var link = svg.selectAll('path') | |
| .data(linksArray) | |
| .enter().append('path') | |
| .attr('fill', 'none') | |
| .attr('stroke', '#F89406') | |
| .attr('stroke-width', function(d) {return d.count}) | |
| .attr('stroke-opacity', 0.25); | |
| var circle = svg.selectAll('g') | |
| .data(usersArray) | |
| .enter().append('g') | |
| .on('mouseenter', hoverNode) | |
| .on('mouseleave', unhoverNode) | |
| .call(force.drag); | |
| circle.append('circle') | |
| .attr('fill-opacity', 0.5) | |
| .attr('fill', '#0088CC') | |
| .attr('r', function(d) { | |
| d.radius = radiusScale(d.length); | |
| return d.radius; | |
| }); | |
| circle | |
| .append('text') | |
| .attr('opacity', function(d) {return d.radius > radius / 8 ? 1 : 0}) | |
| .attr('text-anchor', 'middle') | |
| .style('pointer-events', 'none') | |
| .text(function(d) {return d.username}); | |
| force.nodes(usersArray).links(linksArray); | |
| force.start(); | |
| function updatePositions() { | |
| circle.attr('transform', function(d) { | |
| return 'translate(' + d.x + ',' + d.y + ')'; | |
| }); | |
| link.attr('d', linkArc); | |
| } | |
| // link arc function from https://bl.ocks.org/mbostock/1153292 | |
| function linkArc(d) { | |
| var dx = d.target.x - d.source.x, | |
| dy = d.target.y - d.source.y, | |
| dr = Math.sqrt(dx * dx + dy * dy); | |
| return "M" + d.source.x + "," + d.source.y + | |
| "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; | |
| } | |
| function hoverNode(d) { | |
| var username = d.username; | |
| var highlightedNodes = {}; | |
| // when they hover node, highlight the node | |
| // and nodes it's attached to | |
| link.attr('stroke-opacity', function(d) { | |
| if (d.source.username === username || | |
| d.target.username === username) { | |
| highlightedNodes[d.source.username] = 1; | |
| highlightedNodes[d.target.username] = 1; | |
| return .75; | |
| } | |
| return 0; | |
| }); | |
| circle.selectAll('circle') | |
| .attr('fill-opacity', function(d) { | |
| return highlightedNodes[d.username] ? .75 : 0; | |
| }); | |
| circle.selectAll('text') | |
| .attr('opacity', function(d) { | |
| return highlightedNodes[d.username] ? 1 : 0; | |
| }); | |
| } | |
| function unhoverNode() { | |
| link.attr('stroke-opacity', 0.25); | |
| circle.selectAll('circle') | |
| .attr('fill-opacity', 0.5); | |
| circle.selectAll('text') | |
| .attr('opacity', function(d) {return d.radius > radius / 8 ? 1 : 0}); | |
| }; | |
| }); | |
| </script> | |
| </body> |