Пузырьковая диаграмма

Пузырьковая диаграмма в контексте визуализации данных представляет собой способ отображения многомерной информации через окружности, где каждая окружность (пузырёк) кодирует значения с помощью координат, размера и, при необходимости, цвета. В экосистеме D3.js данный тип визуализации реализуется через комбинацию масштабов, геометрических примитивов и алгоритмов упаковки или симуляции.


Базовая модель пузырьковой диаграммы

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

  • положение по оси X
  • положение по оси Y
  • радиус окружности

Дополнительно может использоваться четвёртое измерение — цвет или прозрачность.

Формально радиус часто вычисляется через функцию масштабирования площади:

[ r = ]

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


Подготовка данных

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

const data = [
  { name: "A", value: 120 },
  { name: "B", value: 60 },
  { name: "C", value: 200 },
  { name: "D", value: 90 }
];

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


Создание SVG-контейнера

В D3.js визуализация строится поверх SVG-элемента:

const width = 800;
const height = 600;

const svg = d3.select("body")
  .append("svg")
  .attr("width", width)
  .attr("height", height);

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


Масштабирование значений

Для корректного отображения значений применяется масштаб:

const radiusScale = d3.scaleSqrt()
  .domain([0, d3.max(data, d => d.value)])
  .range([0, 50]);

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


Простейшая реализация пузырьковой диаграммы

Начальный вариант без алгоритмов упаковки:

svg.selectAll("circle")
  .data(data)
  .enter()
  .append("circle")
  .attr("cx", (d, i) => i * 120 + 100)
  .attr("cy", height / 2)
  .attr("r", d => radiusScale(d.value))
  .attr("fill", "steelblue")
  .attr("opacity", 0.7);

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


Добавление подписей

Текстовые элементы синхронизируются с кругами:

svg.selectAll("text")
  .data(data)
  .enter()
  .append("text")
  .attr("x", (d, i) => i * 120 + 100)
  .attr("y", height / 2)
  .attr("text-anchor", "middle")
  .attr("dy", ".3em")
  .text(d => d.name)
  .style("fill", "white");

Текст размещается по центру пузырьков с использованием text-anchor.


Использование pack layout

Для плотной упаковки окружностей применяется алгоритм d3.pack, предназначенный для иерархических данных.

Структура данных преобразуется в иерархию:

const root = d3.hierarchy({ children: data })
  .sum(d => d.value);

Построение иерархической упаковки

const pack = d3.pack()
  .size([width, height])
  .padding(5);

const nodes = pack(root).leaves();

Каждый узел получает координаты x, y и радиус r.


Отрисовка через pack layout

svg.selectAll("circle")
  .data(nodes)
  .enter()
  .append("circle")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y)
  .attr("r", d => d.r)
  .attr("fill", "tomato")
  .attr("opacity", 0.75);

Этот метод обеспечивает автоматическое размещение без пересечений.


Цветовое кодирование

Добавление цветового измерения позволяет расширить информативность:

const color = d3.scaleOrdinal()
  .domain(data.map(d => d.name))
  .range(d3.schemeTableau10);

Применение цвета:

.attr("fill", d => color(d.data.name))

Подписи в pack-диаграмме

svg.selectAll("text")
  .data(nodes)
  .enter()
  .append("text")
  .attr("x", d => d.x)
  .attr("y", d => d.y)
  .attr("text-anchor", "middle")
  .attr("dy", ".3em")
  .text(d => d.data.name)
  .style("font-size", "10px")
  .style("fill", "white");

Альтернативный подход: force simulation

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

const simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(width / 2))
  .force("y", d3.forceY(height / 2))
  .force("collide", d3.forceCollide(d => radiusScale(d.value) + 2));

Обновление позиций в симуляции

simulation.on("tick", () => {
  svg.selectAll("circle")
    .data(data)
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);
});

Force layout обеспечивает естественное распределение объектов без наложений.


Масштабирование и адаптивность

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

const widthScale = d3.scaleLinear()
  .domain([0, data.length])
  .range([50, width - 50]);

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


Иерархические пузырьки и вложенность

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


Поведение при больших объёмах данных

При увеличении количества элементов возникают ограничения:

  • снижение читаемости при плотной упаковке
  • рост вычислительной сложности force-системы
  • необходимость фильтрации или агрегации

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


Комбинирование с интерактивностью

Интерактивные элементы добавляются через обработчики событий:

.on("mouseover", function(event, d) {
  d3.select(this)
    .attr("stroke", "black")
    .attr("stroke-width", 2);
})
.on("mouseout", function() {
  d3.select(this)
    .attr("stroke", null);
});

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