Создавая визуализации или интерактивные страницы, мы часто используем комбинацию из jQuery и D3. Причём в основном используется D3, а из jQuery берут небольшой набор функций для манипуляций с DOM.

И хотя в D3 есть мощные возможности – селекторы и обёртка для ajax, часто нам не хватает каких-то функций из jQuery. Мы покажем, как можно заменить jQuery, используя D3 повсеместно. В результате ваш код упростится, объём проекта уменьшится, и вы не будете смешивать разные подходы, а будете использовать функции так, как принято в D3.

Для начала рассмотрим, в чём эти две библиотеки сходятся. Это удобно для тех, кто уже знает jQuery, и хочет изучить D3.

Схожести


Селекторы

Обе библиотеки основаны на простых в использовании, но богатых на возможности селекторах.

jQuery

$('.foo').addClass('foobar');
$('.foo').removeClass('foobar');


D3

d3.selectAll('.foo').classed('foobar', true);  
d3.selectAll('.foo').classed('foobar', false); 


Управление стилями и атрибутами

jQuery

$('.foo').attr('data-type', 'foobar');
$('.foo').css('background', '#F00');


D3

d3.selectAll('.foo').attr('data-type', 'foobar');  
d3.selectAll('.foo').style('background', '#F00');  


Ajax

Синтаксис немного отличается, но, как у D3, так и у jQuery есть хорошие обёртки для
ajax.

jQuery

$.getJSON('http://url-to-resource.json', doSomething);
$.ajax({
    url: 'http://url-to-resource.txt',
    dataType: 'text',
    type: 'GET',
    success: doSomething
});


D3

d3.json('http://url-to-resource.json', doSomething);  
d3.text('http://url-to-resource.txt', doSomething);  


Управление классами

Часто бывает необходимо управлять классами элементов DOM, например, чтобы переключать стили.

jQuery

$('.foo').addClass('foobar');
$('.foo').removeClass('foobar');


D3

d3.selectAll('.foo').classed('foobar', true);  
d3.selectAll('.foo').classed('foobar', false);  


Append и Prepend

Вставка дочерних узлов – функция важная, особенно при визуализации входных данных. Это делать легко и просто:

jQuery

$('.foo').append('<div/>');
$('.foo').prepend('<div/>');


D3

d3.selectAll('.foo').append('div');  
d3.selectAll('.foo').insert('div');  


Отслеживание событий

Одинаковый синтаксис предназначен для отслеживания событий на выбранных элементах.

jQuery

$('.foo').on('click', clickHandler);


D3

d3.selectAll('.foo').on('click', clickHandler);  


Удаление элементов

Иногда вам требуется удаление элементов из DOM. Вот как это делается:

jQuery

$('.foo').remove();


D3

d3.selectAll('.foo').remove();  


Выборка подмножества элементов

Вы можете выбрать дочерние элементы из более крупной выборки

jQuery

$('.foo').find('.bar');


D3

d3.selectAll('.foo').selectAll('.bar');  


Управление содержимым

Для изменения содержимого узла DOM можно использовать следующие функции.

jQuery

$('.foo').text('Hello World!');
$('.foo').html('<div class="bar">Hello</div>');


D3

d3.selectAll('.foo').text('Hello World!');  
d3.selectAll('.foo').html('<div class="bar">Hello</div>');  


Различия


Теперь рассмотрим функции, которые есть в jQuery, но отсутствуют в D3. Для каждой из них приводится простое решение на её замену, а также более общий вариант использования, который может пригодиться вам в любом месте вашего приложения, использующий фирменные цепочки D3.

Активация событий и настраиваемые события (trigger events and custom events)

Одно из преимуществ jQuery – удобство работы с событиями. Можно запускать или отслеживать настраиваемые события для любого элемента на странице. К примеру, можно запустить настраиваемое событие с некими данными для вашего документа, и отслеживать его в коде:

//слушаем
$(document).on('dataChange', function(evt, data) {
    //do something with evt and data
    console.log(data.newData);
});

//включаем событие
$(document).trigger('dataChange', {
    newData: 'Hello World!'
});


В D3 это не поддерживается напрямую, но всегда можно добиться такого поведения. Простой вариант (если в хэндлере вам не нужно d3.event):

//слушаем
d3.select(document).on('dataChange', function(data) {  
    console.log(d3.event); //null
    console.log(data.newData);
});

//включаем событие
d3.select(document).on('dataChange')({  
    newData: 'Hello World!'
});


Более общий подход – добавить функцию в объект d3, чтобы её можно было использовать на любой выборке.

d3.selection.prototype.trigger = function(evtName, data) {  
  this.on(evtName)(data);
}


Добавление этой функции в D3 позволяет вам получить функцию-триггер, напоминающую таковую в jQuery, которую можно использовать следующим образом:

d3.select(document).on('dataChange', function(data) {  
  console.log(data);
});


d3.select(document).trigger('dataChange', {newData: 'HelloWorld!'});

after() и before()

При помощи jQuery можно вставлять элементы сразу после всех элементов выборки. Рассмотрим код:

<ul>  
    <li>List</li>
    <li>List</li>
    <li>List</li>
</ul>  


Можно использовать следующий простой код для вставки нового элемента после каждого элемента списка:

$('li').after('<li>Item</li>');


И вот что мы получим:

<ul>  
    <li>List</li>
    <li>Item</li>
    <li>List</li>
    <li>Item</li>
    <li>List</li>
    <li>Item</li>
</ul>  


В D3 придётся пройти по всем элементам выборки и добавить их через JavaScript:

d3.selectAll('li').each(function() {  
  var li = document.createElement('li');
  li.textContent = 'Item';
  this.parentNode.insertBefore(li, this.nextSibling);
})


Лучший вариант – сделать функцию общего назначения, добавляющую элементы на основании имени тэга и возвращающую новую выборку из созданных элементов, чтобы затем их можно было редактировать:

d3.selection.prototype.after = function(tagName) {  
  var elements = [];

  this.each(function() {
    var element = document.createElement(tagName);
    this.parentNode.insertBefore(element, this.nextSibling);
    elements.push(element);
  });

  return d3.selectAll(elements);
}


Добавив следующее в ваш код, можно будет использовать функцию after() почти так же, как это делается в jQuery:

d3.selectAll('li')  
    .after('li')
    .text('Item')
    //тут можно сделать со вставленными элементами что-нибудь ещё


Функция before() выглядит почти также, с той лишь разницей, что элементы вставляются до выборки.

d3.selection.prototype.before = function(tagName) {  
  var elements = [];

  this.each(function() {
    var element = document.createElement(tagName);
    this.parentNode.insertBefore(element, this);
    elements.push(element);
  });

  return d3.selectAll(elements);
}


empty()

Это просто — функция jQuery удаляет все дочерние узлы в выборке.

  • List-Item
  • List-Item
  • List-Item


$('ul').empty();


И в результате:

<ul></ul>  


В D3 для этого нужно очистить внутренний HTML у выбранного элемента:

d3.selectAll('ul').html('');  


D3 часто используют для работы с SVG. В этом случае такой код не сработает, поскольку там не поддерживается innerHTML. Поэтому лучше не вызывать html(), а выбрать все дочерние узлы и удалить их:

d3.selectAll('ul').selectAll('*').remove();  


Код общего назначения будет простой. Я выбрал для функции другое имя, нежели используемое в jQuery, поскольку в D3 уже есть своя функция empty().

d3.selection.prototype.clear = function() {  
  this.selectAll('*').remove();
  return this;
}


Теперь можно очищать выборку почти так же, как в jQuery:

d3.selectAll('#foo').clear();  


appendTo()

В jQuery эта функция работает почти так же, как функция append() в D3, но она добавляет предшествующие выбранные элементы в другую выборку. Чтобы сделать это в D3, необходимо пройти по всем элементам в обоих выборках и добавить элементы друг к другу. Если у вас есть несколько целей, к которым надо добавлять выборку, придётся склонировать объекты, чтобы получить поведение, схожее с jQuery. Вот что у меня получилось:

d3.selection.prototype.appendTo = function(selector) {  
  var targets = d3.selectAll(selector),
      targetCount = targets[0].length,
      _this = this,
      clones = [];

  targets.each(function() {
    var currTarget = this;
    _this.each(function() {
      if(targetCount > 1) {
        var clone = this.cloneNode(true);
        currTarget.appendChild(clone);
        clones.push(clone);
      }
      else {
        currTarget.appendChild(this);
      }
    });
  });

  if(targetCount > 1) {
    this.remove();
  }

  return clones.length > 0 ? d3.selectAll(clones) : this;
}


Используя её, можно добавлять множественные элементы в DOM. Пример работы:

<div class="target"></div>  
<div class="target"></div>

<div class="foo">some element</div>  
<div class="foo">some other element</div>  


Теперь вызываем appendTo() на всех элементах, у которых есть класс «foo», чтобы добавить их к целям.

d3.selectAll('.foo').appendTo('.target');  


Что будет в DOM:

<div class="target">  
    <div class="foo">some element</div>
    <div class="foo">some other element</div>
</div>

<div class="target">  
    <div class="foo">some element</div>
    <div class="foo">some other element</div>
</div>  


Функция возвращает добавленные элементы, чтобы с ними можно было работать и далее. К примеру, изменение фона:

d3.selectAll('.foo').appendTo('.target').style('background', '#f00');

length()

Иногда полезно знать, сколько элементов есть в вашей выборке. в jQuery есть свойство по имени length

<div class=".foo"></div>  
<div class=".foo"></div>  


$('.foo').length; //2


То же самое – в D3:

d3.selection.prototype.length = function() {  
  return this[0].length;
}


С таким кодом можно делать так:

d3.selectAll('.foo').length() //2  


toggleClass()

Как уже говорилось, в D3 можно использовать функцию classed, чтобы управлять именами классов. Но в D3 нет функции для переключения имён классов, которая частенько используется в jQuery. Её реализация может быть такой:

d3.selection.prototype.toggleClass = function(className) {  
      this.classed(className, !this.classed(className));
      return this;
}


eq()

Чтобы отфильтровать выборку нескольких элементов и выбрать только узел с заданным индексом, в jQuery можно использовать функцию eq(). Её довольно просто сделать и для D3. Делаем подвыборку из элементов на основании индекса и возвращаем заново сделанную выборку:

d3.selection.prototype.eq = function(index) {  
  return d3.select(this[0][index]);
}


show() / hide() / toggle()

Используются для изменения видимости элемента на странице. Они просто меняют стили у выбранных элементов. А в функции toggle() сначала необходимо проверить, виден ли данный элемент.

Показать скрытый:

d3.selection.prototype.show = function() {  
  this.style('display', 'initial');
  return this;
}


Спрятать видимый:

d3.selection.prototype.hide = function() {  
  this.style('display', 'none');
  return this;
}


Переключить видимость:

d3.selection.prototype.toggle = function() {  
  var isHidden = this.style('display') == 'none';
  return this.style('display', isHidden ? 'inherit' : 'none');
}


moveToFront(), moveToBack()

Этих функций часто не хватает в D3, но с jQuery они не связаны. D3 часто используют для работы с SVG. При этом, в отличие от HTML, в SVG порядок элементов определяет их видимость. Поэтому часто нам не хватает функциональности для перемещения выборки в SVG назад или вперёд.

Для этого мы расширим выборки D3 следующими функциями:

d3.selection.prototype.moveToFront = function() {  
  return this.each(function(){
    this.parentNode.appendChild(this);
  });
};

d3.selection.prototype.moveToBack = function() {  
    return this.each(function() { 
        var firstChild = this.parentNode.firstChild; 
        if (firstChild) { 
            this.parentNode.insertBefore(this, firstChild); 
        } 
    });
};


Использовать их крайне просто – выбираем элемент svg и двигаем его, куда нужно:

d3.select('svg rect')  
    .moveToFront()
    .moveToBack();


Надеемся, что вышеописанное окажется полезным в тех проектах, в которых излишнее использование jQuery можно заменить простыми решениями на D3. А уже расширенную версию D3 с включёнными функциями можно взять на GitHub.

Комментарии (16)


  1. Wowa69
    17.08.2015 00:18
    +5

    Тогда лучше обёртку для jQuery на D3 сделать, что бы написанный ранее код работал без переработок.


  1. masterclass
    17.08.2015 09:29
    +7

    Так как пишу иногда на jQuery, решил почитать внимательно пост, а вдруг D3 также хорош (тем более про него раньше не слышал). Но прочитав пост, понял что цель

    код упростится, объём проекта уменьшится

    абсолютно не достигнута. В чем преимущества использования D3 из статьи непонятно.


    1. werktone
      17.08.2015 10:07
      +10

      d3 изначально предназначен не для манипуляций с DOM, а для создания интерактивных визуализаций данных. А смысл статьи — в том, что если используется связка d3 + jQuery, то от использования jQuery можно избавиться почти безболезненно.


      1. kuber
        17.08.2015 13:04

        >> d3 изначально предназначен не для манипуляций с DOM
        Откуда информация? На http://d3js.org/ написано следующее:
        … combining powerful visualization components and a data-driven approach to DOM manipulation.


        1. tushev
          17.08.2015 13:08
          +1

          Лучше сказать, для манипуляций с DOM в области data-driven визуализаций, а не просто абстрактных и универсальных DOM манипуляций.


    1. tushev
      17.08.2015 13:06
      +2

      Пост для тех кто уже пользуется D3 и понимает область ее применения.
      D3 это не замена jQuery, хотя во многом возможности пересекаются. Область применения D3 сильно уже чем у jQuery, но в области интерактивных визуализаций данных она существенно удобнее.


    1. dom1n1k
      17.08.2015 16:54
      +3

      Что тут непонятного? Если D3 всё равно используется, то тянуть ещё в дополнение к ней jQ становится бессмысленным.


  1. denis_g
    17.08.2015 10:17

    В общем, на сегодняшний день, с учетом адекватной поддержки стандартов основными браузерами, проще написать свою функции для выполнения необходимых задач на Vanilla JS, чем иметь головную боль с обеими библиотеками.


    1. andy128k
      17.08.2015 11:23
      +2

      Нет смысла писать свои функции. В большинстве случаев можно использовать DOM API напрямую.


  1. jgunchenko
    17.08.2015 10:40
    +1

    Статья хороша для тех, у кого в проекте уже используется D3. Менять jQuery для манипуляций с DOM на него не имеет смысла, лучше уж все переписать на чистом VanillaJS и избавиться от лишних библиотек вообще.


  1. deQU
    17.08.2015 11:56
    +4

    лучше использовать библиотеки по своему назначению — jQuery для манипуляций с DOM, аякса и тд, а D3 — для визуализации данных


  1. Ashot
    17.08.2015 12:34

    Это просто — функция jQuery удаляет все дочерние узлы в выборке.

    Вообще не совсем уж и просто: .empty() не только удаляет узлы, но и подчищает дынные и обработчики событий на этих узлах. Так что заменять его на простой .html('') не совсем корректно — неполноценная замена получается.


  1. negus
    17.08.2015 12:48
    -1

    А вот еще полезная ссылка.

    Вообще стоит подготовиться к изменениям! jQuery и ее подход к манипуляциям с DOM уже выходят из тренда, при использовании Angular 2 и Polymer, например, трудно представить, как ее можно применить.


    1. tushev
      17.08.2015 13:12

      Разве уходят из тренда. Некоторое время назад был большой всплеск интереса к Angular и им подобных подходам. Потом наступил спад, когда поняли что это не панацея. Сейчас Angular просто занял свою нишу и все.


  1. student_ivan
    18.08.2015 11:41
    +1

    Вопрос — чем вам плох document.querySelector?
    Можно даже сделать так $$ = document.querySelector


  1. Matrosked
    18.08.2015 13:27
    +2

    Это пост о пользе чтения документации selectors из d3, я не ошибся?