Skip to content

Instantly share code, notes, and snippets.

@cool-Blue
Last active September 28, 2015 08:07
Show Gist options
  • Save cool-Blue/f810911f5f84b94f2e3e to your computer and use it in GitHub Desktop.
Save cool-Blue/f810911f5f84b94f2e3e to your computer and use it in GitHub Desktop.
d3 Force directed graph with node size transitions - CSS transitions

d3 Force directed graph with node size transitions - CSS transitions


Key points

  1. Gravity, and charge set to 0, friction set to 1
  2. Schedule the transition on the radius in the timer callback
  3. Create a dynamic radius on the data (d.rt) and, in the tick callback, update it with the current DOM element radius to synchronise with the transition
  4. Use the dynamic radius for calculating collisions
  5. Use the dynamic radius to adjust the line x0 and x1 in the tick callback
  6. Use a transform on the nodes (g element) to decouple text and line positioning from node position, adjust the transform x and y, only in the tick callback
  7. Using CSS transitions on circle radius and getComputedStyle or r.animVal.value to track it

Cross-browser version without CSS transitions


Issues

Only works in chrome!

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>http://stackoverflow.com/questions/32521887/animate-objects-in-force-layout-in-d3-js/32523428#32523428</title>
<link rel="stylesheet" type="text/css" href="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.css">
<style>
body {
background: black;
margin:0;
padding:0;
}
#bubble-cloud {
background: url("http://dummyimage.com/100x100/111/333?text=sample") 0 0;
width: 960px;
height: 470px;
/*overflow: hidden;*/
position: relative;
margin:0 auto;
}
#bubble-cloud .circle.inflate {
background: url("http://dummyimage.com/100x100/111/333?text=sample") 0 0;
-webkit-transition: all 1s cubic-bezier(.21,1,.44,1.35);
-moz-transition: all 1s cubic-bezier(.21,1,.44,1.35);
-o-transition: all 1s cubic-bezier(.21,1,.44,1.35);
transition: all 1s cubic-bezier(.21,1,.44,1.35);
}
#bubble-cloud .circle.deflate {
background: url("http://dummyimage.com/100x100/111/333?text=sample") 0 0;
-webkit-transition: all 2s cubic-bezier(0.2, 0.82, 0.165, 1);
-moz-transition: all 2s cubic-bezier(0.2, 0.82, 0.165, 1);
-o-transition: all 2s cubic-bezier(0.2, 0.82, 0.165, 1);
transition: all 2s cubic-bezier(0.2, 0.82, 0.165, 1);
}
#bubble-cloud .line { /* doesn't work! */
-webkit-transition: all 2s ease-out;
-o-transition: all 2s ease-out;
-moz-transition: all 2s ease-out;
transition: all 2s ease-out;
}
</style>
</head>
<body>
<div id="bubble-cloud"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-2.0.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/plot-transform.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.js"></script>
<script>
// helpers
var random = function(min, max) {
if (max == null) {
max = min;
min = 0;
}
return min + Math.floor(Math.random() * (max - min + 1));
},
metrics = d3.select('#bubble-cloud').append("div")
.attr("id", "metrics")
.style("white-space", "pre"),
elapsedTime = outputs.ElapsedTime("#metrics", {
border: 0, margin: 0, "box-sizing": "border-box",
padding: "0 0 0 6px", background: "black", "color": "orange"
})
.message(function(value) {
var this_lap = this.lap().lastLap, aveLap = this.aveLap(this_lap)
return 'alpha:' + d3.format(" >7,.3f")(value)
+ '\tframe rate:' + d3.format(" >4,.1f")(1 / aveLap) + " fps"
}),
hist = d3.ui.FpsMeter("#metrics", {display: "inline-block"}, {
height: 10, width: 100,
values: function(d){return 1/d},
domain: [0, 60]
}),
// mock data
colors = [
{
fill: 'rgba(242,216,28,0.3)',
stroke: 'rgba(242,216,28,1)'
},
{
fill: 'rgba(207,203,196,0.3)',
stroke: 'rgba(207,203,196,1)'
},
{
fill: 'rgba(0,0,0,0.2)',
stroke: 'rgba(100,100,100,1)'
}
];
// initialize
var container = d3.select('#bubble-cloud');
var containerWidth = 960;
var containerHeight = 470 - elapsedTime.selection.node().clientHeight;
var svgContainer = container
.append('svg')
.attr('width', containerWidth)
.attr('height', containerHeight);
var data = [],
rmin = 30,
rmax = 60;
d3.range(0, 3).forEach(function(j){
d3.range(0, 8).forEach(function(i){
var r = random(rmin, rmax);
data.push({
text: 'text' + i,
category: 'category' + j,
x: random(rmax, containerWidth - rmax),
y: random(rmax, containerHeight - rmax),
r: r,
rt: r,
fill: colors[j].fill,
stroke: colors[j].stroke,
get v() {
var d = this;
return {x: d.x - d.px || 0, y: d.y - d.py || 0}
},
set v(v) {
var d = this;
d.px = d.x - v.x;
d.py = d.y - v.y;
},
get s() {
var v = this.v;
return Math.sqrt(v.x * v.x + v.y * v.y)
},
set s(s1){
var s0 = this.s, v0 = this.v;
if(!v0 || s0 == 0) {
var theta = Math.random() * Math.PI * 2;
this.v = {x: Math.cos(theta) * s1, y: Math.sin(theta) * s1}
} else this.v = {x: v0.x * s1/s0, y: v0.y * s1/s0};
},
set sx(s) {
this.v = {x: s, y: this.v.y}
},
set sy(s) {
this.v = {y: s, x: this.v.x}
},
});
})
});
// collision detection
// derived from http://bl.ocks.org/mbostock/1748247
function collide(alpha, s0) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var drt = d.rt;
boundaries(d, drt);
var r = drt + rmax,
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 = drt + quad.point.rt;
if (l < r) {
l = (l - r) / l * (1 + 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;
});
};
function boundaries(d, _drt) {
var moreThan, v0,
drt = _drt || d.rt;
// boundaries
//reflect off the edges of the container
// check for boundary collisions and reverse velocity if necessary
if((moreThan = d.x > (containerWidth - drt)) || d.x < drt) {
d.escaped |= 2;
// if the object is outside the boundaries
// manage the sign of its x velocity component to ensure it is moving back into the bounds
if(~~d.v.x) d.sx = d.v.x * (moreThan && d.v.x > 0 || !moreThan && d.v.x < 0 ? -1 : 1);
// if vx is too small, then steer it back in
else d.sx = (~~Math.abs(d.v.y) || Math.min(s0, 1)*2) * (moreThan ? -1 : 1);
// clear the boundary without affecting the velocity
v0 = d.v;
d.x = moreThan ? containerWidth - drt : drt;
d.v = v0;
// add a bit of hysteresis to quench limit cycles
} else if (d.x < (containerWidth - 2*drt) && d.x > 2*drt) d.escaped &= ~2;
if((moreThan = d.y > (containerHeight - drt)) || d.y < drt) {
d.escaped |= 4;
if(~~d.v.y) d.sy = d.v.y * (moreThan && d.v.y > 0 || !moreThan && d.v.y < 0 ? -1 : 1);
else d.sy = (~~Math.abs(d.v.x) || Math.min(s0, 1)*2) * (moreThan ? -1 : 1);
v0 = d.v;
d.y = moreThan ? containerHeight - drt : drt;
d.v = v0;
} else if (d.y < (containerHeight - 2*drt) && d.y > 2*drt) d.escaped &= ~4;
}
}
// prepare layout
var force = d3.layout
.force()
.size([containerWidth, containerHeight])
.gravity(0.001)
.charge(100)
.friction(.8)
.on("start", function() {
elapsedTime.start(100);
});
// load data
force.nodes(data)
.start();
// create item groups
var node = svgContainer.selectAll('.node')
.data(data)
.enter()
.append('g')
.attr('class', 'node')
.call(force.drag);
// create circles
var circles = node.append('circle')
.classed('circle', true)
.attr('r', function (d) {
return d.r;
})
.style('fill', function (d) {
return d.fill;
})
.style('stroke', function (d) {
return d.stroke;
})
.each(function(d){
// add dynamic r getter
var n= this;
Object.defineProperty(d, "rt", {get: function(){
return parseFloat(window.getComputedStyle(n).getPropertyValue("r")) || +n.r.animVal.value
}})
});
// create labels
node.append('text')
.text(function(d) {
return d.text
})
.classed('text', true)
.style({
'fill': '#ffffff',
'text-anchor': 'middle',
'font-size': '10px',
'font-weight': 'bold',
'text-transform': 'uppercase',
'font-family': 'Tahoma, Arial, sans-serif'
})
.attr('x', function (d) {
return 0;
})
.attr('y', function (d) {
return - rmax/5;
});
node.append('text')
.text(function(d) {
return d.category
})
.classed('category', true)
.style({
'fill': '#ffffff',
'font-family': 'Tahoma, Arial, sans-serif',
'text-anchor': 'middle',
'font-size': '8px'
})
.attr('x', function (d) {
return 0;
})
.attr('y', function (d) {
return rmax/4;
});
var lines = node.append('line')
.classed('line', true)
.attr({
x1: function (d) {
return - d.r + rmax/10;
},
y1: function (d) {
return 0;
},
x2: function (d) {
return d.r - rmax/10;
},
y2: function (d) {
return 0;
}
})
.attr('stroke-width', 1)
.attr('stroke', function (d) {
return d.stroke;
});
// put circle into movement
force.on('tick', function t(e){
var s0 = 0.25, k = 0.3;
a = e.alpha ? e.alpha : force.alpha();
elapsedTime.mark(a);
if(elapsedTime.aveLap.history.length)
hist(elapsedTime.aveLap.history);
for ( var i = 0; i < 2; i++) {
circles
.each(collide(a, s0));
}
lines
.attr({
x1: function (d) {
return -d.rt + rmax/10;
},
x2: function (d) {
return d.rt - rmax/10;
}
});
lines.each(function(d){
var drt = d.rt;
d3.select(this)
.attr({
x1: -drt + rmax/10,
x2: drt - rmax/10
});
})
// regulate the speed of the circles
data.forEach(function reg(d){
if(!d.escaped) d.s = (s0 - d.s * k) / (1 - k);
});
node.attr("transform", function(d){return "translate(" + [d.x, d.y] + ")"});
force.alpha(0.05);
});
// animate
var interval = setInterval(function(){
for(var i = 0; i < data.length; i++) {
if(Math.random()>0.6) data[i].r = random(rmin,rmax);
}
circles.filter(function(d){return !d.scheduled && d.r != d.rt})
//.transition("r").duration(2000)
.attr('r', function (d, i) {
var inflate = d.r > d.rt;
d.scheduled = true;
d3.select(this)
.classed("inflate", inflate)
.classed("deflate", !inflate)
return d.r;
})
.on("transitionend", function(d){
window.setTimeout(function(){
d.scheduled = false;
// console.log("\t\tend " + d.category + " " + d.text)
}, d3.select(this).classed("inflate") ? 1000 : 2000);
});
}, 2 * 1000);</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment