Clik here to view.

Authored by Steven Hall
Continuing on from my last post.
Here we are going to walk through a couple of demos that use D3 style selections in THREE.js. If you didn't read the first part of this series, I'm trying out the selection data structure for rending in WebGL with THREE.js. We're using a ~600 line library I put together from the original D3 selection code that let's you test drive the idea. It allows you to "select" into the scene graph the same way you would in the DOM and use the same syntax.
View the code on Github
You can also just bower install the library and all the demos. The instructions are on the GitHub page.
Full Set of Demos HERE
Demos in this post
Image may be NSFW.Clik here to view.

Simple Example
Starting from some familiar ground with a basic bar chart.
View the demo here
Image may be NSFW.
Clik here to view.

More Complex Example
Building on the previous example to show selections at work in THREE.js and how to create more complex structures.
View the demo here
Image may be NSFW.
Clik here to view.

Sphere Example
Taking it more into the 3D world using THREE.js positioning tools and materials.
View the demo here
Quick Digression on ES6 Modules
The library and all the demos are made up of ES6 modules. The demo modules are compiling together in the browser using the Traceur runtime. I think this adds to the value of the demos. I'm only using the simple "import" and "export" syntax and not all the new language features like generators, so pretty much anybody should be able to follow the idea. The "main" module is in a script tag on each demo page. That's all the key code that uses selections. Helper functions and supporting stuff gets pushed to modules. For example, the "scene" module loads all the boiler-plate camera, canvas, renderer, etc and it's the same across all the demos. It's being held constant.
If you're not familiar with ES6 modules take a look at this starter page.
Making a Bar Chart in WebGL
Ok. Here we go. To be clear, if all you need is a bar chart then you don't need WebGL or THREE.js. We're doing it this way to demonstrate how selections can be used in THREE.js and using a bar chart is good common ground to begin from. This is about demonstrating a way to work with THREE.js and WebGL. These bars (meshes) could be anything: complex geometries, tweets, asteroids, animated 3D models, images, etc.
You may want to reference the classic example from Mike Bostock as a refresher. We're going to use the same data throughout the examples. It looks like this...
var data = [ {"letter": "A", "frequency": 0.08167}, {"letter": "B", "frequency": 0.01492}, {"letter": "C", "frequency": 0.02780}, ];
Here's the way this looks using selections in THREE.js and rendering in WebGL...
// NOTE: size = [width, height] for the chart var root = SubUnit.select(scene); // hello selections root.node().position.set(-size[0] / 2, 0, 0); var bars = root.selectAll("bar") .data(data).enter() .append("mesh") // append on meshes .attr("tags", "bar") // discussed below .attr("material", blue) .attr("geometry", function (d) { var w = x.rangeBand(); var h = size[1] - y(d.frequency); return new THREE.BoxGeometry(w, h, 5); }) .each(function (d) { var x0 = x(d.letter); var y0 = - y(d.frequency) / 2; this.position.set(x0, y0, 240); // position the mesh }) .on('click', function (event, d) { // event handler d3.select("#msg").html("Letter: " + d.letter); if (this.material === blue) { this.material = grey; } else { this.material = blue; } });
Take a look at the live example
NOTE: The "root" is being put on the window in these examples. In the console try something like...
root.selectAll("bar") .attr("material", new THREE.MeshBasicMaterial({color: '#ff0000'})); or root.selectAll("bar") .filter(function (d) { return d.frequency > 0.03; }) .attr("material", new THREE.MeshBasicMaterial({color: '#ffffff'}));
This gets the basic idea across, but we still aren't really using selections to their full potential. Let's make four charts and stack them up on the screen to better demonstrate how you can build more complicated structures that wouldn't approximate just a for loop. Then we'll go a little bigger in the last example.
A More Complex Example
Starting from the exact same code that made the bar chart, let's redefine the data so we have four charts like so...
data = [ // Redefine the data for multiple charts {color: '#739D34', data: d3.shuffle(data).slice()}, {color: '#A1A838', data: d3.shuffle(data).slice()}, {color: '#2F4E00', data: d3.shuffle(data).slice()}, {color: '#4F5400', data: d3.shuffle(data).slice()} ];
Ok, we've got an array of four objects with a "color" and a "data" property. Let's go.
We want to create four charts with a backing and a set of bars. Grouping items together would really be helpful to help get things positioned correctly. In SVG you can establish a new grouping and coordinate system using a "g" element. There is no "g" in WebGL or THREE.js. Instead you use a 3D object to accomplish the same goal. In the code below, when it says append("object"), a THREE.Object3D is being inserted into the scene and then the backing and the bars get appended to that "chart" object. That way we only have to move the backing and bars relative to the chart's location because they inherit its position. This makes things a whole lot easier.
The code for the four charts looks like this...
// NOTE: size = [width, height] for the chart var root = SubUnit.select(scene); // select scene var container = root.append("object"); // create container var charts = container.selectAll("chart") .data(data).enter() .append("object") .tagged("chart", true) .each(function (d, i) { this.position.y = i * size[1]; // position the charts }); charts.append("mesh") // append backing to each chart .attr("tags", "backing") .attr("material", function (d) { var opts = {color: d.color, map: metal}; return new THREE.MeshPhongMaterial(opts); }) .attr("geometry", backing) .each(function (d, i) { this.position.z = -50; // just need to move it relative to the chart this.position.x = size[0] / 2; }); var bars = charts.selectAll("bar") // append on the bars .data(function (d) {return d.data;}).enter() .append("mesh") .attr("tags", "bar") .attr("material", color1) .attr("geometry", function (d) { var w = x.rangeBand(); var h = size[1] - y(d.frequency); return new THREE.BoxGeometry(w, h, 5); }) .each(function (d, i) { var x0 = x(i) + x.rangeBand() / 2; var y0 = -y(d.frequency) / 2; this.position.set(x0, y0, 0); // position the bar }) .on('click', function (event, d) { d3.select("#msg").html("Letter: " + d.letter); if (this.material === color1) { this.material = color2; } else { this.material = color1; } }); container.node().position.x = - size[0] / 2; container.node().position.y = (-size[1] * 2) + size[1] / 2;
Look at the live demo here.
I added some fancy materials and added some rotation and there you have it. If you view source on the example you can see that in the render function we can do stuff like...
charts.each(function (d) { this.rotation.x += 0.005; });
Selecting Into the Scene
When you select into a THREE.js scene, there are no CSS style selectors anymore like we have in the DOM. If you work with D3 you know using classes is pretty handy for semantically grouping items and being able to select them back out later or tag items that match a particular criteria. To keep that same flow, SubUnit uses "tags" that operate in the same way classes work in D3. You can put an unlimited number of tags on an item and select them back out of the scene by inspecting those tags. An object's tags are an array of strings.
You select with...
// NOTE: the leading dot (.) like you use with classes in CSS // is not needed. Just between tags. root.selectAll("bar") // or "bar.active" or "bar.active.blue"
Then you can set a tag on the object in the same basic way you do it in D3.
bars.tagged("active", true) // equivalent to "classed" in D3 // OR bars.tagged("active", function (d) { return d.frequency > 0.05; }) // OR root.attr("tags", "active") // OR root.attr("tags", "node active") // two tags
But you can do more than just work with tags when selecting into the scene. You can also select by any of the mesh's properties as well. In the console on the demo try this...
// Make two materials var red = new THREE.MeshBasicMaterial({color: '#ff0000'}); var blue = new THREE.MeshBasicMaterial({color: '#0000ff'}); // Change some bars to red root.selectAll("bar") .filter(function (d) { return d.frequency > 0.02; }) .attr("material", red); // Select them back out, make them blue root.selectAll({material: red}) .attr("material", blue);
If you're less familiar with D3 selections it might not be obvious that these queries against the scene are being handled efficiently. In the code above, when you say...
var bars = charts.selectAll("bar")
That is not running a query four times against the entire scene. It is running four separate traversals from the "chart" nodes looking for objects with the tag "bar." Since in the code above the backings (added immediately before) are the only children of each chart when the query runs it only checks those items and returns without needlessly looking through the rest of the scene. In a more complex scenario this can be really important to avoid traversing a ridiculous number of objects.
But this is still basically a 2D example. Let's take it 3D by bumping the data up a notch, laying out the charts on sphere and changing the materials a little.
Arranging Items on a Sphere
Image may be NSFW.Clik here to view.

To get more mileage out of 3D let's make a chart for each item in the data (a total of 26 charts) and place them on a sphere. Then we'll change the materials and add some rotations for effect.
This could be quite a challenging operation if you're doing this with no help and no experience with 3D. We need to move the charts into position AND orient them correctly in 3D space. I have a function that places objects evenly on a sphere (which you can just Google or use the one I stole from Mr Doob), but the more challenging thing is getting objects oriented correctly. THREE.js provides just these types of tools for translating objects and transforming matrices, doing most of the hard work for you. In this case I can just use the lookAt method on the Mesh object ("this" in the function) to have all the charts facing towards the center. In this case I want to use the "container" node. Like so...
var charts = container.selectAll("chart") .data(data).enter() .append("object") .tagged("chart", true) .each(function (d, i) { var position = sphere(i, data.length, 1600); // position this.position.copy(position); this.lookAt(container.node().position); // rotation });
Then we do the following when we create the bars. To generate more meshes, we are returning the full data set for each item in said set. That looks like this...
var bars = charts.selectAll("bar") .data(function (d) { return data; }).enter() // return data .append("mesh") .attr("tags", "bar") .attr("material", barMtrl) .attr("geometry", function (d) { var w = x.rangeBand(); var h = size[1] - y(d.frequency); return new THREE.BoxGeometry(w, h, 5); })
So we're going to get 26 charts, one for each letter of the alphabet, and a total of 676 bars (26 X 26).
View the demo here
Well, there you have it. All these demos and a few more are in the repo.
Check the repo out on GitHub here.
Comments, questions, and suggestions are welcomed.