.data(): привязка массива к выборке

Метод .data() в D3.js является ключевым механизмом связывания данных с DOM-элементами. Он формирует основу декларативного подхода библиотеки, при котором структура документа определяется не вручную через императивные операции создания и удаления узлов, а через соответствие между массивом данных и набором элементов в выборке.

В основе работы .data() лежит сопоставление элементов массива с DOM-узлами выборки. Каждое значение из массива связывается с конкретным элементом, формируя внутреннюю структуру данных внутри DOM через скрытое поле __data__.

const data = [10, 20, 30];

d3.select("body")
  .selectAll("div")
  .data(data);

На этом этапе D3 не обязательно создаёт или удаляет элементы. Метод лишь устанавливает соответствие между данными и уже существующими элементами выборки. Если элементов меньше, чем данных, часть значений остаётся не привязанной и попадает в специальную подсекцию enter.

Три состояния: enter, update, exit

После вызова .data() выборка разделяется на три логические группы:

  • update — элементы, которым соответствует существующий DOM-узел
  • enter — новые данные без соответствующих элементов
  • exit — лишние элементы без соответствующих данных

Эта модель позволяет декларативно описывать изменения DOM в зависимости от состояния данных.

const selection = d3.select("body")
  .selectAll("p")
  .data([1, 2, 3, 4]);

Если в DOM было меньше <p>, чем элементов в массиве, часть значений попадёт в enter.

Обработка enter-состояния

Для работы с новыми данными используется метод .enter(), который возвращает виртуальные элементы, не привязанные к DOM, но представляющие новые данные.

const data = [1, 2, 3, 4];

const selection = d3.select("body")
  .selectAll("p")
  .data(data);

selection.enter()
  .append("p")
  .text(d => d);

В этом примере для каждого отсутствующего элемента создаётся новый <p>, после чего данные отображаются через функцию доступа d => d.

Обновление существующих элементов

Часть выборки, относящаяся к update, используется для изменения уже существующих DOM-узлов. Это позволяет избегать пересоздания элементов при изменении данных.

const data = [5, 10, 15];

d3.selectAll("p")
  .data(data)
  .text(d => d);

Здесь текст существующих элементов обновляется в соответствии с новым массивом.

Удаление лишних элементов (exit)

Когда данных становится меньше, чем элементов в DOM, часть узлов попадает в exit-секцию. Их необходимо явно удалить.

const data = [1, 2];

const selection = d3.selectAll("p")
  .data(data);

selection.exit()
  .remove();

Удаление через .remove() является обязательным шагом, иначе DOM сохранит устаревшие элементы.

Функции доступа и контекст данных

Каждый элемент, связанный через .data(), получает доступ к своему значению через функцию-обработчик. Сигнатура функции доступа:

function(d, i) {}

где:

  • d — текущий элемент данных
  • i — индекс элемента в массиве
d3.selectAll("li")
  .data([100, 200, 300])
  .text((d, i) => `Index ${i}: ${d}`);

Такой подход позволяет учитывать как значение данных, так и позицию в массиве.

Ключи (key function) и стабильная привязка

По умолчанию привязка данных осуществляется по индексу. Это может приводить к нежелательным пересозданиям элементов при изменении порядка массива. Для решения используется функция ключа.

const data = [
  { id: "a", value: 10 },
  { id: "b", value: 20 }
];

d3.selectAll("div")
  .data(data, d => d.id);

Функция d => d.id определяет стабильную идентификацию элементов. Теперь D3 сопоставляет данные не по позиции, а по уникальному ключу, что позволяет корректно обрабатывать перестановки, вставки и удаления.

Поведение при обновлении массива

Рассмотрим изменение структуры данных:

let data = [
  { id: 1, value: 10 },
  { id: 2, value: 20 }
];

const selection = d3.selectAll("div")
  .data(data, d => d.id);

При обновлении:

data = [
  { id: 2, value: 25 },
  { id: 3, value: 30 }
];

selection.data(data, d => d.id);
  • элемент с id: 2 обновляется
  • элемент с id: 1 попадает в exit
  • элемент с id: 3 попадает в enter

Такое поведение позволяет минимизировать DOM-операции и сохранять визуальную непрерывность интерфейса.

Связь данных с DOM через data

D3 внутренне хранит привязанные данные в свойстве __data__ каждого DOM-узла.

const node = document.querySelector("div");
console.log(node.__data__);

Хотя прямое использование этого свойства не является частью публичного API, оно демонстрирует механизм хранения состояния.

Объединение enter и update

Часто требуется единый поток обработки для новых и существующих элементов. Для этого используется метод .merge().

const data = [1, 2, 3];

const update = d3.selectAll("div")
  .data(data);

update.enter()
  .append("div")
  .merge(update)
  .text(d => d);

Такой подход объединяет enter и update в одну цепочку, позволяя применять одинаковые стили и поведение.

Привязка сложных структур данных

Метод .data() работает не только с примитивами, но и с объектами любой сложности.

const data = [
  { name: "alpha", value: 10 },
  { name: "beta", value: 20 }
];

d3.selectAll("section")
  .data(data)
  .text(d => `${d.name}: ${d.value}`);

Структуры данных могут включать вложенные объекты, массивы и вычисляемые поля, что делает .data() универсальным механизмом отображения состояния приложения.

Поведение при повторной привязке данных

Повторный вызов .data() на той же выборке пересчитывает соответствие:

const selection = d3.selectAll("p");

selection.data([1, 2, 3]);
selection.data([4, 5]);

Каждая операция полностью перестраивает внутренние связи между DOM и массивом, включая пересчёт enter, update и exit.

Роль .data() в архитектуре D3

Метод .data() является центральным звеном между данными и визуальным представлением. Он обеспечивает:

  • синхронизацию состояния DOM с массивом данных
  • минимизацию прямых DOM-операций
  • поддержку декларативного обновления интерфейса
  • основу для построения масштабируемых визуализаций

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