|
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'); |
|
} |