Автор: , перевод Тани Мисютиной, Димы Тихвинского и Димы Бибикова

Мыслим связками

Скажем, вы собираетесь показать данные SVG-кругами на двумерном графике. Вот вы удивитесь, узнав, что в D3 нет примитивов для создания группы DOM-элементов. Погодите, ЧО?

Для создания одного элемента используется метод append:

svg.append("circle")
    .attr("cx", d.x)
    .attr("cy", d.y)
    .attr("r", 2.5);

Но это один круг, а нужно несколько — по одному для каждого значения. Прежде чем прибегнуть к помощи цикла for, изучите эти таинственные строки:

svg.selectAll("circle")
    .data(data)
  .enter().append("circle")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; })
    .attr("r", 2.5);

Они делают ровно то, что нужно: создают по одному SVG-кругу для каждого элемента данных, используя свойства x и y в качестве координат. Но почему мы выделяем заведомо несуществующие элементы вместо того, чтобы просто создать новые? ЧО?

А вот что: вместо того, чтобы указывать D3 как сделать что-то, сообщите, что именно вам нужно. Вам нужны круги, связанные с данными — по одному кругу для каждого элемента данных. Для этого достаточно сообщить D3, что выборка "circle" связана с данными. Этот принцип называется связкой элементов с данными.

Данные, связанные с уже существующими элементами, образуют выборку update (пересечение кругов). Оставшиеся данные образуют выборку enter (слева), которая представляет недостающие элементы. Соответственно, элементы без данных образуют выборку exit (справа), предназначенную для удаления.

Принцип связки элементов с данными поможет нам разобраться в таинственной последовательности enter – append:

  1. svg.selectAll("circle") возвращает пустую выборку, т.к. SVG-контейнер был пуст. Родительский узел для этой выборки — элемент SVG.

  2. Пустая выборка связывается с массивом данных, порождая три выборки: enter, update и exit.

  3. Метод selection.data по умолчанию возвращает выборку update; для обращения к выборкам enter и exit используются методы selection.enter и selection.exit соотвественно.

  4. Недостающие элементы добавляются в родительский узел вызовом selection.append на выборке enter. Так в SVG-контейнере появляется по кругу для каждого элемента данных.

Мыслить связками — значит объявить связь между выборкой элементов и данными, а затем управлять ей с помощью состояний enter, update и exit.

Но к чему такие сложности? Почему не создать группу элементов одной командой? Красота связок — в обобщении. Приведённый код обрабатывает лишь состояние enter, этого достаточно для статических визуализаций. Для создания динамической визуализации достаточно добавить обработку состояний update and exit. После этого вы сможете отображать данные в режиме реального времени, добавлять интерактивное взаимодействие, плавно анимировать переходы!

Вот пример обработки всех трёх состояний:

var circle = svg.selectAll("circle")
    .data(data);

circle.exit().remove();

circle.enter().append("circle")
    .attr("r", 2.5);

circle
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });

При каждом запуске код создаёт новую связку — определяет соотношения между данными и свг-кругами. Если новый массив данных меньше старого, ненужные круги попадают в выборку exit и будут удалены функцией remove. В противном случае, избыточные данные попадают в выборку enter, на которой функция append создаёт новые круги. Если размеры массивов совпадают, круги не появятся и не исчезнут, изменятся только их координаты.

Использование связок делает код более декларативным: все три состояния обрабатываются без использования ветвлений (if) и итераций (for). Если выборки enter, update или exit окажутся пустыми, описывающий их трансформации код попросту не выполнится.

Связки позволяют производить операции на конкретной выборке. Можно присвоить постоянное значение (например, атрибут "r" — радиус круга) только вновь добавленным элементам (enter). Засчёт повторных выборок и минимизации изменений в DOM-дереве, отрисовка страницы значительно ускоряется!

Аналогично задается анимация для конкретной выборки. Например, плавное появление вновь добавленных элементов:

circle.enter().append("circle")
    .attr("r", 0)
  .transition()
    .attr("r", 2.5);

Или исчезновение удалённых:

circle.exit().transition()
    .attr("r", 0)
    .remove();

Теперь вы умеете мыслить связками!

Приложение

В дополнение к этой заметке Майк описал стандартный алгоритм обновления данных.