📜 ⬆️ ⬇️

Visualize in 3D, or how to make friends D3 and Three.js

If you have already heard about D3 and Three.js , this article may seem interesting to you. It focuses on how to make these libraries work together to create dynamic three-dimensional scenes, for example, this simple histogram:




')

Where do legs grow from?



Some time ago we in CodeOrchestra experimented with port D3 on AS3 / DSL codenamed “D6” (from D3 + 3D). Our port covered only the most basic functions of the D3, but it was able to work with popular 3D engines on AS3 out of the box. And although we have not brought D6 to light, the very idea of ​​using D3 for 3D has not left our minds since. Indeed, if you look in the gallery D3 , you will not find there a single three-dimensional example. The reason is that D3 is heavily sharpened to work with the browser DOM, and it seems that it does not support sampling of arbitrary objects. However, with sufficient motivation, we can force it.


So, let's begin



Let's start with the simplest example of a two-dimensional histogram using D3 (hereinafter the code is adapted from the official lessons of D3 [ 1 ] and [ 2 ], and shortened for readability):

d3.select(".chart") .selectAll() .data(data) .enter().append("div") .style("width", function(d) { return d * 10 + "px"; }); 

In this example, you can see that the main D3 methods take as arguments the magic DOM-dependent strings (such as the selector .chart or the name of the div tag), which is extremely inconvenient for our purposes. Fortunately, these methods have alternative signatures. These signatures exist for boring things like sample reuse. We will use them to rewrite our example as follows:

 function newDiv() { return document.createElement("div"); } var chart = { appendChild: function (child) { //      append()  newDiv() return document.getElementById("chartId") .appendChild(child); }, querySelectorAll: function () { //      selectAll() return []; } } d3.select( chart ) .selectAll() .data(data) .enter().append( newDiv ) .style("width", function(d) { return d * 10 + "px"; }); 

As you can see, we 1) told D3 how to create the div explicitly, and 2) convinced D3 that our chart object is a duck . The result of our code has not changed at all.


So what about 3D?



The de facto standard for 3D graphics in JavaScript today is Three.js. If we want to do 3D in D3, we need to convince D3 to work with samples from three-dimensional Three.js objects in a similar way. To do this, we will add the following methods to the Object3D prototype:

 //     D3- .append()  .selectAll() THREE.Object3D.prototype.appendChild = function (c) { this.add(c); return c; }; THREE.Object3D.prototype.querySelectorAll = function () { return []; }; //   -  D3- .attr() 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; } 

This is quite enough to create a simple three-dimensional histogram :

 function newBar () { return new THREE.Mesh( geometry, material ); } chart3d = new THREE.Object3D(); //  D3   3D  d3.select( chart3d ) .selectAll() .data(data) .enter().append( newBar ) .attr("position.x", function(d, i) { return 30 * (i - 3); }) .attr("position.y", function(d, i) { return d; }) .attr("scale.y", function(d, i) { return d / 10; }) 



It's all ?



Not at all. To use the main feature of D3 - data modification processing - we need to revise our frauds. First of all, in order for D3 to interpolate the “attribute” values, we need to add the getAttribute method to the Object3D prototype:

 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]]; } 

Secondly, selectAll () should actually work in order to build a sample of updated objects. For example, we can implement a selection of the successors of a certain type of Object3D:

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

To make our columns dance , now it is enough just to periodically change the data:

 var N = 9, v = 30, data = d3.range(9).map(next); function next () { return (v = ~~Math.max(10, Math.min(90, v + 20 * (Math.random() - .5)))); } setInterval(function () { data.shift(); data.push(next()); update(); }, 1500); function update () { //  D3     3D  var bars = d3.select( chart3d ) .selectAll("THREE.Mesh") .data(data); bars.enter().append( newBar ) .attr("position.x", function(d, i) { return 30 * (i - N/2); }); bars.transition() .duration(1000) .attr("position.y", function(d, i) { return d; }) .attr("scale.y", function(d, i) { return d / 10; }); } 

So, the general principle of pairing D3 with Three.js should be clear to you - we gradually add Object3D methods to the prototype that are sufficient for the functionality of the D3 functionality that we are interested in. But for fixing, we consider the last version of the histogram, in which we use data binding by key and work with a sample of deleted objects. Add the removeChild method to the Object3D prototype:

 THREE.Object3D.prototype.removeChild = function (c) { this.remove(c); } 

If you tried now to use the remove () method of selecting objects to be deleted, you would find that nothing happens. Why? The answer is easy to see in the source code of D3 - the remove () method does not use the parentNode of the sample, but tries to remove an object from its immediate parent. To make this possible, we need to adjust our implementation of appendChild ():

 THREE.Object3D.prototype.appendChild = function (c) { this.add(c); //   parentNode c.parentNode = this; return c; } 



Total



But in the end we got this beauty :

 var N = 9, t = 123, v = 30, data = d3.range(9).map(next); function next () { return { time: ++t, value: v = ~~Math.max(10, Math.min(90, v + 20 * (Math.random() - .5))) }; } function update () { //  D3  ,    3D  var bars = d3.select( chart3d ) .selectAll("THREE.Mesh") .data(data, function(d) { return d.time; }); bars.transition() .duration(1000) .attr("position.x", function(d, i) { return 30 * (i - N / 2); }) bars.enter().append( newBar ) .attr("position.x", function(d, i) { return 30 * (i - N / 2 + 1); }) .attr("position.y", 0) .attr("scale.y", 1e-3) .transition() .duration(1000) .attr("position.x", function(d, i) { return 30 * (i - N / 2); }) .attr("position.y", function(d, i) { return d.value; }) .attr("scale.y", function(d, i) { return d.value / 10; }) bars.exit().transition() .duration(1000) .attr("position.x", function(d, i) { return 30 * (i - N / 2 - 1); }) .attr("position.y", 0) .attr("scale.y", 1e-3) .remove() } 

As you can see, D3 does an excellent job with 3D if it helps a little, and Three.js does not create any problems with this. Both libraries have their strengths, and I hope that this article has opened you the path to their harmonious combination in your future work.

Source: https://habr.com/ru/post/200584/


All Articles