Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active August 29, 2015 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/37f3b0b206cabef55e3f to your computer and use it in GitHub Desktop.
Save nitaku/37f3b0b206cabef55e3f to your computer and use it in GitHub Desktop.
WebVis contributions bubble chart

(This gist contains server-side code. In order to see it running, you should open it on WebVis.)

This example displays a simple bubble chart, in which each bubble is sized according to the amount of contributions a certain user has made to the WebVis laboratory. Data is read live from the underlying Neo4j graph DB through PHP.

The bubble chart is implemented as a force layout with a non-overlapping constraint (see this example), initialized with a radial displacement of the circles. A simpler, more efficient implementation using a circle packing layout can be found in this example.

<?php
header('Content-Type: application/json');
require("phar://neo4jphp.phar");
$client = new Everyman\Neo4j\Client('127.0.0.1', 7474);
$contributors = array();
$contributors_result = (new Everyman\Neo4j\Cypher\Query($client,
"
MATCH (u:User)-[r:OWNS|CONTRIBUTED_TO]->()
RETURN u AS user, count(r) AS contributions
ORDER BY contributions DESC
"
))->getResultSet();
foreach ($contributors_result as $row) {
$contributor = array(
'name' => $row['user']->getProperties()['name'],
'user_github_id' => $row['user']->getProperties()['github_id'],
'contributions' => $row['contributions']
);
array_push($contributors, $contributor);
}
// return the resulting list as JSON
echo json_encode($contributors);
?>
svg {
background: #333;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.min.js"></script>
<link rel="stylesheet" type="text/css" href="index.css">
<title>WebVis contributions bubble chart</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
var width = 960,
height = 500,
padding = 10,
min_padding = 0,
max_padding = 50,
maxRadius = 120;
var circles, nodes, force;
d3.json('get_contributors.php', function(data) {
var radius_scale = d3.scale.sqrt()
.domain([0, d3.max(data, function(d){ return d.contributions; })])
.range([0, maxRadius]);
nodes = data.map(function(d, i){
var c = {
id: d.name,
radius: radius_scale(d.contributions),
cx: Math.cos(i*2*Math.PI/data.length)*radius_scale(d.contributions),
cy: Math.sin(i*2*Math.PI/data.length)*radius_scale(d.contributions),
avatar_id: d.user_github_id
};
c.x = c.cx;
c.y = c.cy;
return c;
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append('defs');
create_avatar_patterns(nodes);
var vis = svg.append('g')
.attr('transform', 'translate('+width/2+','+height/2+')');
circles = vis.selectAll("circle")
.data(nodes);
var enter_circle = circles.enter().append("circle")
.attr('class', 'node');
enter_circle
.attr("r", function(d) { return d.radius; })
.attr('transform', function(d) { return 'translate('+d.x+','+d.y+')'; })
.attr('fill', function(d) { return 'url(#user_pattern_'+d.avatar_id+')'; });
force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.start();
for(var i=0; i<1000; i++) {
tick();
}
force.stop();
circles
.attr('transform', function(d) { return 'translate('+d.x+','+d.y+')'; });
});
function tick() {
circles
.each(gravity(.2 * force.alpha()))
.each(collide(.5));
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
function create_avatar_patterns(nodes) {
var user_patterns = d3.select('defs').selectAll('.user_patterns')
.data(nodes);
user_patterns.enter()
.append('pattern')
.attr('class', 'user_patterns')
.attr('id', function(d){ return 'user_pattern_' + d.avatar_id; })
.attr('patternUnits', 'userSpaceOnUse')
.attr('x', function(d){ return -d.radius; })
.attr('y', function(d){ return -d.radius; })
.attr('width', function(d){ return 2*d.radius; })
.attr('height', function(d){ return 2*d.radius; })
.append('image')
.attr('xlink:href', function(d){ return 'http://avatars3.githubusercontent.com/u/' + d.avatar_id; })
.attr('x', 0)
.attr('y', 0)
.attr('width', function(d){ return 2*d.radius; })
.attr('height', function(d){ return 2*d.radius; })
.attr('preserveAspectRatio', 'xMidYMid slice');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment