So, our little D3 + 3D experiment continues. This time we will be trying to replicate bar chart tutorial 2 in 3D.
Transitions
Two things need to be done before D3 transitions could work. First, D3 must be able to interpolate “attribute” values, so we need to implement both setAttribute and getAttribute methods in THREE.js:
// this one is to use D3's .attr() on THREE's objects THREE.Object3D.prototype.setAttribute = function (name, value) { var chain = name.split('.'); var object = this; for (var i = 0; i < chain.length - 1; i++) { object = object[chain[i]]; } object[chain[chain.length - 1]] = value; } // and this one is to make'em work with D3's .transition()-s THREE.Object3D.prototype.getAttribute = function (name) { var chain = name.split('.'); var object = this; for (var i = 0; i < chain.length - 1; i++) { object = object[chain[i]]; } return object[chain[chain.length - 1]]; }
Second, D3’s selectAll() now needs to actually select something to build updating selection. We can make it select various descendants of Object3D type, for example:
// now selectAll must actually do something THREE.Object3D.prototype.querySelectorAll = function (selector) { var matches = []; var type = eval(selector); for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child instanceof type) { matches.push(child); } } return matches; }
These changes are already enough to replicate their simple transition example in THREE.js:
var t = 1297110663, v = 30, data = d3.range(9).map(next); function next() { return { time: ++t, value: v = ~~Math.max(10, Math.min(90, v + 10 * (Math.random() - .5)) ) }; } setInterval(function() { data.shift(); data.push(next()); redraw(); }, 1500); function redraw() { d3.select( chart3d ) .selectAll("THREE.Mesh") .data(data) .transition() .duration(1000) .attr("position.y", function(d, i) { return d.value; }) .attr("scale.y", function(d, i) { return d.value / 10; }) }
But to replicate their final keyed data join example, we need to take care of one last thing…
Exit selections
We want D3’s remove() to actually remove 3D objects. Now, you might be thinking, this is easy one, we’ll just add removeChild() to Object3D prototype
THREE.Object3D.prototype.removeChild = function (c) { this.remove(c); }
and it should work, right? Wrong, it does not! To solve this one, I had to check D3 source. Turns out D3 does not call removeChild on selection parentNode but on node’s parentNode itself, so we need to modify our appendChild as follows:
THREE.Object3D.prototype.appendChild = function (c) { this.add(c); // create parentNode property c.parentNode = this; return c; }
Final result
With these changes, it is now possible to do this:
function redraw() { var bars = d3.select( chart3d ) .selectAll("THREE.Mesh") .data(data, function(d) { return d.time; }); // move existing bars to their new place bars.transition() .duration(1000) .attr("position.x", function(d, i) { return 30 * i; }) // add new bar and make it grow bars.enter().append( newBar ) .attr("position.x", function(d, i) { return 30 * (i + 1); }) .attr("position.y", 0) .attr("scale.y", 1e-3) .transition() .duration(1000) .attr("position.x", function(d, i) { return 30 * i; }) .attr("position.y", function(d, i) { return d.value; }) .attr("scale.y", function(d, i) { return d.value / 10; }) // remove the bar that is no longer in data set bars.exit().transition() .duration(1000) .attr("position.x", function(d, i) { return 30 * (i - 1); }) .attr("position.y", 0) .attr("scale.y", 1e-3) .remove() }
This is still not complete THREE.js “plugin” but, as you can see, it’s a nice start.
Wow, nice work! I was just wondering if d3 could be extended into 3 dimenions, and here, lo and behold, you’ve figured a lot of it out.
This is excellent. Thanks so much! I noticed that your implementation of querySelectorAll doesn’t look recursively at children of children. Could you suggest a way to change this?
Try nested functions (something like this).
Awesome. Thanks!