Last active
August 29, 2015 14:15
-
-
Save tzellman/463cb44a4b28e70d8b5e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
svg circle { | |
cursor: pointer; | |
fill-opacity: 0.2; | |
} | |
</style> | |
<body> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.1.0/lodash.min.js"></script> | |
<script src="http://marak.com/faker.js/js/faker.min.js"></script> | |
<script> | |
// assume these are passed in from somewhere (e.g in an Ember component) | |
var width = 1024, | |
data = [ | |
{label: "1", value: 10}, | |
{label: "2", value: 20}, | |
{label: "3", value: 15}, | |
{label: "4", value: 5}, | |
{label: "5", value: 5} | |
], | |
height = 800, | |
rows = 2, | |
strokeWidth = 3; | |
var bound = Math.min(width, height), | |
nodes = [], | |
foci = [], | |
fill = d3.scale.category10(), | |
_id = 0, | |
cols = Math.ceil((data.length) / rows), | |
fociBound = {x: Math.floor(width / cols), y: Math.floor(height / rows)}, | |
// add padding in between foci | |
padding = {x: Math.floor(width / 30), y: 0}, | |
//add a margin at the beginning | |
margin = {x: Math.floor(width / 15), y: Math.floor(height / 15)}; | |
var svg = d3.select("body").append("svg") | |
.attr("height", height) | |
.attr("width", width); | |
data.forEach(function (d, i) { | |
d.foci = i; | |
d._id = _id++; | |
nodes.push(d); | |
var row = Math.floor(i / cols), | |
col = Math.floor(i % cols), | |
yCenter = row * fociBound.y + Math.floor(fociBound.y / (row === 0 ? 2 : 3)) + row * padding.y, | |
xCenter = col * fociBound.x + Math.floor(fociBound.x / rows) + col * padding.x; | |
var point = {x: xCenter, y: yCenter}; | |
foci.push(point); | |
d.x = foci[d.foci].x; | |
d.y = foci[d.foci].y; | |
}); | |
var values = d3.extent(data, function (n) { | |
return n.value; | |
}); | |
var radius = d3.scale.linear() | |
.domain(values) | |
.range([bound / 16, bound / 8]); | |
var zoomRadius = d3.scale.linear() | |
.domain(values) | |
.range([bound / 12, bound / 6]); | |
var charge = d3.scale.log() | |
.domain(values) | |
.range([-300, -200]); | |
var force = d3.layout.force() | |
.nodes(nodes) | |
.charge(function (n) { | |
return charge(n.value); | |
}) | |
.friction(0.95) | |
.gravity(0.05) | |
.size([width - margin.x * 2, height - margin.y * 2]) | |
.on("tick", tick.bind(this)); | |
var node = svg.selectAll('g.entity') | |
.data(nodes) | |
.enter() | |
.append('g') | |
.attr('class', 'entity') | |
.attr("transform", function (d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}) | |
.call(force.drag); | |
function updateCircle() { | |
d3.select(this) | |
.transition().duration(300) | |
.ease("linear") | |
.attr("r", function (d) { | |
var radFun = d.zoom ? zoomRadius : radius; | |
return radFun.call(null, d.value); | |
}); | |
} | |
node.append('circle') | |
.style("stroke", function (d) { | |
return d3.rgb(fill(d.foci)).darker(2); | |
}) | |
.style("fill", function (d) { | |
return d3.rgb(fill(d.foci)); | |
}) | |
.style("stroke-width", function (d) { | |
return strokeWidth; | |
}) | |
.each(updateCircle); | |
// Suppose the image is square, and is centered in a circle with radius r. | |
// Then, the square image should hit at 45 degrees, meaning that the width | |
// and height should be 2*sin(tao/8) * r | |
var imageWH = function (d) { | |
var radFun = d.zoom ? zoomRadius : radius; | |
return 2 * Math.sin(6.28 / 8) * (radFun.call(null, d.value) - strokeWidth); | |
}; | |
function updateImage() { | |
d3.select(this).transition().duration(300) | |
.ease("linear") | |
.attr("x", function (d) { | |
return -imageWH(d) / 2; | |
}) | |
.attr("y", function (d) { | |
return -imageWH(d) / 2; | |
}) | |
.attr("width", imageWH) | |
.attr("height", imageWH); | |
} | |
node.append("image") | |
.attr("xlink:href", function (d) { | |
return faker.internet.avatar(); | |
}).each(updateImage); | |
node.on('mouseover', function (n) { | |
d3.selectAll("g.entity > circle") | |
.filter(function (d) { | |
return d._id === n._id; | |
}) | |
.each(function (d, i) { | |
d.zoom = true; | |
d3.selectAll("image").each(updateImage); | |
updateCircle.call(this); | |
}); | |
}); | |
node.on('mouseout', function (n) { | |
d3.selectAll("g.entity > circle") | |
.filter(function (d) { | |
return d._id === n._id; | |
}) | |
.each(function (d, i) { | |
d.zoom = false; | |
d3.selectAll("image").each(updateImage); | |
updateCircle.call(this); | |
}); | |
}); | |
force.start(); | |
function tick(e) { | |
var k = 0.1 * e.alpha; | |
// Push nodes toward their designated focus. | |
nodes.forEach(function (o) { | |
o.y += (foci[o.foci].y - o.y) * k; | |
o.x += (foci[o.foci].x - o.x) * k; | |
}); | |
var q = d3.geom.quadtree(nodes), | |
i = 0, | |
n = nodes.length; | |
while (++i < n) { | |
q.visit(collide(nodes[i], radius)); | |
} | |
svg.selectAll("g.entity") | |
.attr('transform', function (n) { | |
var r = radius(n.value); | |
n.x = Math.max(r, Math.min(width - r, n.x, width - margin.x), margin.x); | |
n.y = Math.max(r, Math.min(height - r, n.y, height - margin.y), margin.y); | |
return 'translate(' + n.x + ',' + n.y + ')'; | |
}); | |
} | |
function collide(node, radius) { | |
var r = radius(node.value) + 10, | |
nx1 = node.x - r, | |
nx2 = node.x + r, | |
ny1 = node.y - r, | |
ny2 = node.y + r; | |
return function (quad, x1, y1, x2, y2) { | |
if (quad.point && (quad.point !== node)) { | |
var x = node.x - quad.point.x, | |
y = node.y - quad.point.y, | |
l = Math.sqrt(x * x + y * y), | |
r = radius(node.value) + radius(quad.point.value); | |
if (l < r) { | |
l = (l - r) / l * 0.5; | |
node.x -= x *= l; | |
node.y -= y *= l; | |
quad.point.x += x; | |
quad.point.y += y; | |
} | |
} | |
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1; | |
}; | |
} | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment