Как построить bubble chart с использованием d3.js?

Заказчики бывают разные, соответственно и запросы — разные. Собственно, один из заказчиков и попросил нарисовать с помощью библиотеки красивый bubble-чарт, с физикой и возможностью перегруппировки. О нем и поговорим.

С чего все начиналось

А начиналось все, как и должно, с технического задания. Оно было достаточно полным для того, чтобы начать работу. Была описана бизнес-логика страницы в целом и, в частности сказано, какие параметры пузырька за что отвечают, как можно группировать, фильтровать пузырьки и прочие мелочи, которые, как известно, самые главные. Также был приведен пример, откуда можно взять логику и физику пузырьков.

Приступаем

Копаться в исходниках страницы реального сайта было немного сложно, поэтому был найден источник, описывающий всю физику пузырьков человеческим языком. Хочется сказать, что для человека, который только начал разбираться с D3’s Force Layout, могут показаться сложными понятия friction, charge, gravity. Даже на официальном сайте d3 указывается, что эти названия «возможно вводят в заблуждение». И это так! Скажу вкратце о значениях параметров:

  • friction – параметр, который отвечает за ускорение перемещения частицы (узла DOM). Например, если значение параметра равно 1, то ускорение мгновенное — частица из точки А перемещается в точку Б с быстротой молнии. Если значение будет в диапазоне от 0 до 1, то частице потребуется какое-то время, чтобы разогнаться. Если значение параметра будет равно 0, то частица застынет вовсе и никуда не поедет.
  • charge — параметр, определяющий силу притягивания/отталкивания частиц друг от друга. Скажу по личному опыту — параметр для простых случаев полезный, но в нашей ситуации он оказался камнем преткновения. Но об этом позже.
  • gravity — параметр, отвечающий за то, как сильно притягиваются/отталкиваются частицы от центра полотна.

Открывать Америку я не буду, в блоге автора библиотеки достаточно много примеров использования «сил». Скажу вкратце:

1) Чтобы создать силу, достаточно ее объявить и задать размеры:

this._force = d3.layout.force();
this._force.size([width, height]);

Все остальные параметры задаются автоматически.

2) Чтобы добавить в нее частицы, нужно вызвать метод nodes:

this._force.nodes(data);

data – массив объектов, которые содержат в себе параметры частиц. Объект может содержать в себе любые поля, но некоторые имена полей используются как стартовые значения для силы. Например, координаты x и y точки, которые необходимы для расчета силы.

Чтобы сила двигала частицы так, как нам хочется, нужно определить обработчик события tick. Он срабатывает на каждый «кадр» силы и позволяет просчитать следующее положение частицы. Вот тут-то и начинается магия…

Запуск силы в действие происходит по вызову метода start:

this._force.start();

После вызова этого метода наша сила «оживает».

Обработка «кадра» силы

Как упоминалось выше, именно в обработчике события tick происходит магия — просчет параметров частицы. В нашем случае, у пузырька есть 4 параметра:

  • положение по горизонтали
  • положение по вертикали
  • радиус
  • цвет

Цвет в рамках силы никак не фигурирует, поэтому его мы рассматривать не будем.

Суть обработчика сводится к полному перебору всех элементов DOM (в нашем случае svg:circle), расчете параметров каждого узла и установке нового значения параметра. Обработчик вызывается до тех пор, пока значение alpha (так называемый cooling parameter) не станет равным нулю. По какому конкретно закону оно вычисляется на каждом шаге, знает, наверное, только автор библиотеки.

Сия операция, мягко говоря, не очень легкая. Не рассчитывайте, что случится чудо и вы сможете красиво представлять данные любого размера — все зависит от мощностей клиентской машины и браузера. В нашем случае заказчик долго упрямился и хотел, чтобы все пузырьки красиво летали при входных данных более 10 000 элементов. Мы умерили его пыл, написав простую демо-версию с возможностью выбора количества элементов. По личному наблюдению могу сказать, что в Google Chrome расчет и анимация проходят быстрее, чем в Firefox, но это только на заметку.

Вроде работает, что дальше?

Был реализован подход, предложенный в руководстве, на которое я ссылался выше. Все работает, как и написано в статье, пузырьки красиво расплываются, обтекают друг друга, останавливаются на своих положениях… Но что-то все-таки не так. Если посмотреть на живой пример из статьи, то видно, что сами пузырьки взаимодействуют друг с другом вне зависимости от принадлежности к группе. Из-за этого возникает общая сила, которая толкает группы друг от друга. В примере из статьи всего 3 группы, однако, в нашем случае их было до 7 штук. Поверьте, выглядит это просто ужасно: иногда группа пузырьков была оттолкнута другими группами на 1.5 — 2 деления шкалы. Это вводило пользователя в заблуждение, заставляя высчитывать нужную группу от центральной, если таковая была.

Bubble-чарт: пузырьки взаимодействуют вне зависимости от принадлежности к группе

Решение было найдено, как и ожидалось, у автора библиотеки. В данном случае он не использует стандартную функцию для просчета взаимодействия частиц, выставив параметру charge значение «0». Вместо этого он разбивает частицы на кластеры, причем, каждая из частиц знает, к какому кластеру она принадлежит. Исходя из этого, сила взаимодействия рассчитывается только для частиц, принадлежащих одному кластеру.

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

Bubble-чарт: сила взаимодействия рассчитывается только для пузырьков из одной группы

Заключение

Приводить код полноценного компонента здесь неуместно, так как у каждого свои задачи, да и политика конфиденциальности кода не просто так придумана. Надеюсь, я донес общую идею и разъяснил проблемы, с которыми столкнулся при разработке сего компонента. Возможно, эта статья поможет тем, кто разрабатывает нечто подобное, быстрее написать свой компонент.