Откуда ноги растут?
Некоторое время назад мы в CodeOrchestra экспериментировали с портом D3 на AS3/DSL под кодовым названием «D6» (от D3 + 3D). Наш порт покрывал лишь самые базовые функции D3, но зато умел работать с популярными 3D движками на AS3 «из коробки». И хотя мы так и не вывели D6 в свет, та самая идея использовать D3 для 3D с тех пор не покидает наши умы. Действительно, если Вы заглянете в галерею D3, Вы не найдёте там ни одного трёхмерного примера. Причина в том, что D3 сильно заточена на работу с DOM браузера, и вроде как не поддерживает выборки произвольных объектов. Однако, обладая достаточной мотивацией, мы можем её заставить.
Итак, начнём
Начнём с простейшего примера двухмерной гистограммы с использованием D3 (тут и далее код адаптирован из официальных уроков D3 [1] и [2], и сокращён ради читабельности):
d3.select(".chart") .selectAll() .data(data) .enter().append("div") .style("width", function(d) { return d * 10 + "px"; });
В этом примере видно, что основные методы D3 принимают в качестве аргументов волшебные DOM-зависимые строки (такие как селектор .chart
или имя тега div
), что крайне неудобно для наших целей. К счастью, у этих методов имеются альтернативные сигнатуры. Эти сигнатуры существуют для скучных вещей вроде повторного использования выборок. Мы же воспользуемся ими, чтобы переписать наш пример следующим образом:
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"; });
Как видим, мы 1) указали D3 как создавать div
в явном виде, и 2) убедили D3 в том, что наш объект chart — утка. При этом результат нашего кода совершенно не изменился.
Так что насчёт 3D ?
Стандартом де-факто для 3D графики в JavaScript на сегодняшний день является Three.js. Если мы хотим делать 3D в D3, нам надо аналогичным образом убедить D3 работать с выборками из трёхмерных объектов Three.js. Для этого мы добавим следующие методы в прототип Object3D:
// эти методы нужны для 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; }
Этого вполне достаточно для создания простейшей трёхмерной гистограммы:
function newBar () { return new THREE.Mesh( geometry, material ); } chart3d = new THREE.Object3D(); 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; })
Это всё ?
Вовсе нет. Чтобы использовать главную фишку D3 — обработку изменения данных — нам необходимо пересмотреть наши обманки. Во-первых, чтобы D3 могла интерполировать значения «аттрибутов», нам необходимо добавить в прототип Object3D метод getAttribute:
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]]; }
Во-вторых, selectAll() должен на самом деле работать, чтобы построить выборку обновляющихся объектов. Например, мы можем реализовать отбор наследников 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; }
Чтобы заставить наши столбцы танцевать, теперь достаточно просто периодически изменять данные:
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; }); }
Итак, общий принцип спаривания D3 с Three.js Вам должен быть ясен — мы постепенно добавляем в прототип Object3D методы, достаточные для работы интересующего нас функционала D3. Но для закрепления рассмотрим последний вариант гистограммы, в котором используем привязку данных по ключу и работу с выборкой удаляемых объектов. Добавим в прототип Object3D метод removeChild:
THREE.Object3D.prototype.removeChild = function (c) { this.remove(c); }
Если бы Вы попробовали теперь воспользоваться методом remove() выборки удаляемых объектов, то обнаружили бы, что ничего не происходит. Почему? Ответ легко увидеть в исходниках D3 — метод remove() не использует parentNode выборки, а пытается удалять объект из своего непосредственного родителя. Чтобы сделать это возможным, необходимо скорректировать нашу реализацию appendChild():
THREE.Object3D.prototype.appendChild = function (c) { this.add(c); // создаём свойство parentNode c.parentNode = this; return c; }
Итог
А в итоге у нас получилась вот такая красота:
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() }
Как видим, D3 прекрасно справляется с 3D, если ей немного помочь, а Three.js не создаёт в этом никаких проблем. Обе библиотеки имеют свои сильные стороны, и я надеюсь, что эта статья открыла Вам путь к их гармоничному сочетанию в Ваших будущих работах.
ссылка на оригинал статьи http://habrahabr.ru/post/200584/
Добавить комментарий