Swarm plot (или beeswarm plot) представляет собой способ визуализации распределения точек, при котором значения отображаются вдоль одной оси, а пересечения и наложения устраняются за счёт смещения точек во второй оси. Основная идея заключается в сохранении реального значения по одной координате и предотвращении перекрытия элементов через вычисление их взаимного отталкивания или детерминированного размещения.
Обычная точечная диаграмма (scatter plot) теряет читаемость при высокой плотности данных: точки накладываются друг на друга и скрывают структуру распределения. Beeswarm решает эту проблему через:
Ключевой характеристикой является сохранение исходной осмысленной координаты при добавлении псевдослучайного, но структурированного смещения.
Swarm plot часто рассматривается как более простой вариант:
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);
Наиболее базовый подход — добавление случайного смещения по вертикали:
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);
Недостаток такого метода заключается в недетерминированности и возможных пересечениях при высокой плотности.
Более устойчивый подход реализуется через физическую симуляцию D3.
Основные силы:
forceX — притяжение к фиксированной оси XforceY — группировка по категориям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);
Высокая начальная энергия позволяет быстрее раздвинуть точки, низкая — делает результат более стабильным, но медленным.
Альтернативный подход основан на пошаговом размещении точек.
Алгоритм:
Пример упрощённой логики:
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:
При больших наборах данных ключевыми становятся:
const quadtree = d3.quadtree()
.x(d => d.x)
.y(d => d.y)
.addAll(data);
Quadtree снижает стоимость проверки пересечений с O(n²) до приблизительно O(n log n).
Swarm-распределения тесно связаны с корректной настройкой шкал. Изменение domain или range требует пересчёта симуляции.
Особое значение имеет дискретизация категорий:
Геометрия beeswarm напрямую определяется радиусом столкновения:
Форма распределения становится индикатором плотности данных, что позволяет использовать beeswarm не только как визуализацию точек, но и как способ анализа структуры распределения значений