Swarm plot и beeswarm

Swarm plot (или beeswarm plot) представляет собой способ визуализации распределения точек, при котором значения отображаются вдоль одной оси, а пересечения и наложения устраняются за счёт смещения точек во второй оси. Основная идея заключается в сохранении реального значения по одной координате и предотвращении перекрытия элементов через вычисление их взаимного отталкивания или детерминированного размещения.


Принцип визуализации

Обычная точечная диаграмма (scatter plot) теряет читаемость при высокой плотности данных: точки накладываются друг на друга и скрывают структуру распределения. Beeswarm решает эту проблему через:

  • фиксацию одной координаты (обычно ось X или Y)
  • распределение точек вдоль перпендикулярной оси
  • предотвращение перекрытий с помощью алгоритма упаковки или физической симуляции

Ключевой характеристикой является сохранение исходной осмысленной координаты при добавлении псевдослучайного, но структурированного смещения.


Отличие swarm plot и beeswarm

Swarm plot часто рассматривается как более простой вариант:

  • размещение точек с фиксированным шагом и jitter-распределением
  • отсутствие строгого физического моделирования

Beeswarm plot:

  • использует алгоритмы плотной упаковки
  • учитывает радиусы точек
  • стремится к минимальному вертикальному смещению
  • формирует более «естественное» распределение

В D3.js оба подхода реализуются через разные инструменты: либо ручное вычисление позиций, либо симуляция сил.


Базовая структура данных

Для построения swarm-распределения обычно используется набор объектов:

const data = [
  { value: 10, category: "A" },
  { value: 12, category: "A" },
  { value: 18, category: "B" },
  { value: 22, category: "B" }
];

Значение value фиксирует положение на оси X, категория используется для группировки по оси Y.


Масштабы и базовая компоновка

Для отображения используется стандартная схема масштабирования:

const xScale = d3.scaleLinear()
  .domain(d3.extent(data, d => d.value))
  .range([0, width]);

const yScale = d3.scalePoint()
  .domain([...new Set(data.map(d => d.category))])
  .range([0, height])
  .padding(1);

Простая реализация swarm через jitter

Наиболее базовый подход — добавление случайного смещения по вертикали:

svg.selectAll("circle")
  .data(data)
  .join("circle")
  .attr("cx", d => xScale(d.value))
  .attr("cy", d => yScale(d.category) + (Math.random() - 0.5) * 10)
  .attr("r", 4);

Недостаток такого метода заключается в недетерминированности и возможных пересечениях при высокой плотности.


Beeswarm через force simulation

Более устойчивый подход реализуется через физическую симуляцию D3.

Основные силы:

  • forceX — притяжение к фиксированной оси X
  • forceY — группировка по категориям
  • forceCollide — предотвращение пересечений
const simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(d => xScale(d.value)).strength(1))
  .force("y", d3.forceY(d => yScale(d.category)).strength(0.2))
  .force("collide", d3.forceCollide(4))
  .stop();

Далее выполняется итеративный расчёт:

for (let i = 0; i < 120; i++) simulation.tick();

После завершения симуляции координаты фиксируются:

svg.selectAll("circle")
  .data(data)
  .join("circle")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y)
  .attr("r", 4);

Управление плотностью и радиусами

Ключевым параметром beeswarm является радиус столкновения. Он определяет минимальное расстояние между точками.

d3.forceCollide()
  .radius(4.5)
  .strength(1);

При увеличении радиуса структура становится более разреженной, при уменьшении — возрастает плотность и риск перекрытий.


Группировка по категориям

Для категориальных данных используется дополнительное ограничение по оси Y:

d3.forceY(d => yScale(d.category))
  .strength(0.3);

При высокой силе группировки точки строго фиксируются в пределах полосы категории. При низкой — образуют более «естественные» облака распределения.


Управление устойчивостью симуляции

Симуляция D3 имеет параметры, влияющие на стабильность:

  • alpha — начальная энергия системы
  • alphaDecay — скорость затухания
  • velocityDecay — сопротивление движению
const simulation = d3.forceSimulation(data)
  .alpha(1)
  .alphaDecay(0.05)
  .velocityDecay(0.3);

Высокая начальная энергия позволяет быстрее раздвинуть точки, низкая — делает результат более стабильным, но медленным.


Детерминированный beeswarm без физической симуляции

Альтернативный подход основан на пошаговом размещении точек.

Алгоритм:

  1. Сортировка по оси X
  2. Размещение первой точки в центре категории
  3. Поиск ближайшего свободного положения вокруг базовой линии
  4. Проверка пересечений с уже размещёнными точками

Пример упрощённой логики:

const placed = [];

function intersects(a, b, r) {
  const dx = a.x - b.x;
  const dy = a.y - b.y;
  return dx * dx + dy * dy < (r * 2) * (r * 2);
}

data.sort((a, b) => a.value - b.value);

data.forEach(d => {
  let y = yScale(d.category);
  let offset = 0;

  while (true) {
    const candidate = { x: xScale(d.value), y: y + offset };

    const collision = placed.some(p => intersects(p, candidate, 4));

    if (!collision) {
      placed.push(candidate);
      d.x = candidate.x;
      d.y = candidate.y;
      break;
    }

    offset += 2;
  }
});

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


Работа с плотными распределениями

При больших объёмах данных beeswarm сталкивается с ограничениями визуальной плотности. Возникают характерные эффекты:

  • вертикальное расширение полос категорий
  • вытеснение точек на периферию
  • формирование «дугообразных» структур

Для контроля плотности применяются:

  • ограничение максимального отклонения от оси
  • уменьшение радиуса точек
  • агрегация данных перед отрисовкой

Цветовое кодирование и многомерность

Beeswarm часто используется для отображения дополнительного измерения через цвет:

const color = d3.scaleOrdinal()
  .domain(["A", "B", "C"])
  .range(["#4e79a7", "#f28e2c", "#e15759"]);
.attr("fill", d => color(d.category))

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

  • размером точки
  • прозрачностью
  • формой маркера

Сравнение подходов реализации

Force-based beeswarm:

  • адаптивен к плотности
  • учитывает сложные взаимодействия
  • требует вычислительного времени
  • может давать разные результаты при изменении параметров

Deterministic beeswarm:

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

Оптимизация производительности

При больших наборах данных ключевыми становятся:

  • уменьшение количества итераций симуляции
  • использование предварительной кластеризации
  • ограничение числа столкновений через spatial indexing (например, quadtree)
const quadtree = d3.quadtree()
  .x(d => d.x)
  .y(d => d.y)
  .addAll(data);

Quadtree снижает стоимость проверки пересечений с O(n²) до приблизительно O(n log n).


Взаимодействие с осью и масштабами

Swarm-распределения тесно связаны с корректной настройкой шкал. Изменение domain или range требует пересчёта симуляции.

Особое значение имеет дискретизация категорий:

  • scalePoint подходит для равномерного распределения
  • scaleBand используется при необходимости фиксированной ширины полосы

Влияние радиуса и плотности на форму распределения

Геометрия beeswarm напрямую определяется радиусом столкновения:

  • малый радиус → высокая плотность, риск перекрытий
  • средний радиус → сбалансированное распределение
  • большой радиус → выраженная вертикальная деформация

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