Компонент Bubble (PackedCircles): упаковка кругов

Компонент Bubble из библиотеки Nivo предназначен для визуализации иерархических данных в виде набора вложенных или сгруппированных окружностей. Такой тип диаграмм также называют Packed Circles или Circle Packing. Каждый круг представляет узел дерева, а его размер зависит от числового значения.

Bubble-диаграммы особенно полезны для:

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

Установка

Для работы компонента требуется основной пакет Nivo и пакет Bubble:

npm install @nivo/circle-packing

или:

yarn add @nivo/circle-packing

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

Компонент использует древовидную структуру. Корневой элемент содержит дочерние узлы через свойство children.

Пример данных:

const data = {
    name: 'root',
    children: [
        {
            name: 'Frontend',
            children: [
                {
                    name: 'React',
                    value: 40
                },
                {
                    name: 'Vue',
                    value: 25
                },
                {
                    name: 'Angular',
                    value: 35
                }
            ]
        },
        {
            name: 'Backend',
            children: [
                {
                    name: 'Node.js',
                    value: 50
                },
                {
                    name: 'Python',
                    value: 45
                }
            ]
        }
    ]
}

Первый Bubble-график

ResponsiveBubble

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

import { ResponsiveBubble } from '@nivo/circle-packing'

const MyBubbleChart = () => (
    <div style={{ height: 600 }}>
        <ResponsiveBubble
            data={data}
            margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
            identity="name"
            value="value"
            colors={{ scheme: 'nivo' }}
        />
    </div>
)

Основные свойства

data

Источник данных.

data={data}

identity

Определяет поле, используемое как идентификатор узла.

identity="name"

value

Поле числового значения.

value="value"

Именно это значение определяет размер окружности.


margin

Отступы внутри контейнера.

margin={{
    top: 40,
    right: 40,
    bottom: 40,
    left: 40
}}

padding

Расстояние между кругами.

padding={4}

Большие значения создают более свободную упаковку.


Цветовые схемы

Nivo поддерживает встроенные палитры.

colors={{ scheme: 'category10' }}

Популярные схемы:

  • nivo
  • paired
  • set1
  • accent
  • dark2
  • category10

Собственные цвета

Вместо схемы можно использовать функцию.

colors={(node) => {
    if (node.depth === 1) return '#3b82f6'
    if (node.depth === 2) return '#10b981'
    return '#ef4444'
}}

Работа с глубиной дерева

Каждый узел имеет свойство depth.

  • 0 — корневой элемент;
  • 1 — первый уровень;
  • 2 — второй уровень;
  • далее по иерархии.

Пример использования:

borderWidth={(node) => node.depth === 1 ? 4 : 1}

Настройка границ

borderWidth

Толщина обводки.

borderWidth={2}

borderColor

Цвет границы.

borderColor={{
    from: 'color',
    modifiers: [
        ['darker', 0.6]
    ]
}}

Модификатор darker автоматически затемняет основной цвет.


Подписи внутри кругов

label

Поле для отображения текста.

label="name"

labelTextColor

Цвет текста.

labelTextColor={{
    from: 'color',
    modifiers: [
        ['darker', 3]
    ]
}}

labelSkipRadius

Минимальный радиус, при котором отображается подпись.

labelSkipRadius={18}

Маленькие круги не будут содержать текст.


Анимация

Bubble-компонент поддерживает анимации через библиотеку react-spring.

animate

Включение анимации.

animate={true}

motionConfig

Настройка поведения анимации.

motionConfig="gentle"

Доступные варианты:

  • default
  • gentle
  • wobbly
  • stiff
  • slow
  • molasses

Интерактивность

onClick

Обработка клика по узлу.

onCl ick={(node) => {
    console.log(node)
}}

onMouseEnter

Событие наведения.

onMouseEn ter={(node) => {
    console.log('hover', node)
}}

onMouseLeave

Событие ухода курсора.

onMouseLe ave={() => {
    console.log('leave')
}}

Всплывающие подсказки

По умолчанию компонент показывает tooltip.

Можно создать собственный:

tooltip={({ node }) => (
    <div
        style={{
            padding: 12,
            background: '#222',
            color: '#fff',
            borderRadius: 4
        }}
    >
        <strong>{node.data.name}</strong>
        <div>Value: {node.value}</div>
    </div>
)}

Скрытие корневого элемента

Часто корневой узел используется только как контейнер данных.

root

root={{
    padding: 0
}}

leavesOnly

Показывать только конечные элементы:

leavesOnly={true}

В этом режиме внутренние категории не отображаются как отдельные круги.


Ограничение глубины

childColor

Цвет потомков относительно родителя.

childColor={{
    from: 'color',
    modifiers: [
        ['brighter', 0.4]
    ]
}}

inheritColorFromParent

Наследование цветов.

inheritColorFromParent={true}

Работа с темой

Компоненты Nivo поддерживают единый объект theme.

theme={{
    text: {
        fontSize: 14,
        fill: '#333'
    },
    tooltip: {
        container: {
            background: '#111',
            color: '#fff'
        }
    }
}}

Использование SVG и Canvas

Nivo предоставляет две версии Bubble-компонента:

SVG

ResponsiveBubble

Преимущества:

  • лучше для интерактивности;
  • поддержка DOM;
  • гибкая стилизация.

Недостатки:

  • хуже производительность при тысячах элементов.

Canvas

ResponsiveBubbleCanvas

Преимущества:

  • высокая производительность;
  • подходит для больших объёмов данных.

Недостатки:

  • меньше возможностей кастомизации;
  • сложнее работа с DOM-событиями.

Bubble Canvas

Пример использования Canvas-версии:

import { ResponsiveBubbleCanvas } from '@nivo/circle-packing'

const MyChart = () => (
    <div style={{ height: 700 }}>
        <ResponsiveBubbleCanvas
            data={data}
            identity="name"
            value="value"
            colors={{ scheme: 'set2' }}
            padding={3}
        />
    </div>
)

Кастомизация узлов

renderNode

Позволяет полностью переопределить рендеринг круга.

renderNode={({ node, style }) => (
    <g transform={`translate(${style.x}, ${style.y})`}>
        <circle
            r={style.radius}
            fill={style.color}
            stroke="#000"
        />

        <text
            textAnchor="middle"
            dominantBaseline="central"
            fill="#fff"
        >
            {node.data.name}
        </text>
    </g>
)}

Использование изображений внутри кругов

Bubble часто применяют для отображения аватаров или логотипов.

renderNode={({ node, style }) => (
    <g transform={`translate(${style.x}, ${style.y})`}>
        <clipPath id={`clip-${node.id}`}>
            <circle r={style.radius} />
        </clipPath>

        <image
            href={node.data.image}
            width={style.radius * 2}
            height={style.radius * 2}
            x={-style.radius}
            y={-style.radius}
            clipPath={`url(#clip-${node.id})`}
        />
    </g>
)}

Динамическая загрузка данных

Bubble хорошо подходит для API-данных.

const [data, setData] = useState(null)

useEffect(() => {
    fetch('/api/stats')
        .then(res => res.json())
        .then(setData)
}, [])

Рендеринг:

if (!data) return <div>Loading...</div>

return (
    <ResponsiveBubble
        data={data}
        identity="name"
        value="value"
    />
)

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

Мемоизация данных

const chartData = useMemo(() => transformData(data), [data])

Мемоизация обработчиков

const handleClick = useCallback((node) => {
    console.log(node)
}, [])

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

При большом количестве узлов рекомендуется:

ResponsiveBubbleCanvas

Обработка больших деревьев

При сложной иерархии полезно:

  • ограничивать глубину;
  • скрывать мелкие узлы;
  • уменьшать количество подписей;
  • использовать lazy loading;
  • отключать анимацию.

Пример:

animate={false}
labelSkipRadius={25}

Форматирование значений

valueFormat

valueFormat=">-.2f"

Пример результата:

25.46

Легенды

Bubble-компонент не имеет встроенных легенд, но можно создавать собственные.

const legend = [
    {
        color: '#3b82f6',
        label: 'Frontend'
    },
    {
        color: '#10b981',
        label: 'Backend'
    }
]

Практический пример

Визуализация структуры компании

const data = {
    name: 'company',
    children: [
        {
            name: 'Engineering',
            children: [
                { name: 'Frontend', value: 35 },
                { name: 'Backend', value: 50 },
                { name: 'DevOps', value: 20 }
            ]
        },
        {
            name: 'Marketing',
            children: [
                { name: 'SEO', value: 15 },
                { name: 'Content', value: 25 }
            ]
        },
        {
            name: 'Sales',
            children: [
                { name: 'B2B', value: 40 },
                { name: 'B2C', value: 30 }
            ]
        }
    ]
}

Компонент:

<ResponsiveBubble
    data={data}
    identity="name"
    value="value"
    padding={6}
    colors={{ scheme: 'spectral' }}
    borderWidth={2}
    borderColor={{
        from: 'color',
        modifiers: [['darker', 0.5]]
    }}
    labelSkipRadius={16}
    labelTextColor={{
        from: 'color',
        modifiers: [['darker', 3]]
    }}
    animate={true}
    motionConfig="gentle"
/>

Частые проблемы

Круги не отображаются

Причины:

  • отсутствует value;
  • значение равно 0;
  • неверная структура дерева;
  • отсутствует контейнер с высотой.

Правильно:

<div style={{ height: 600 }}>
    <ResponsiveBubble ... />
</div>

Ошибка иерархии

Bubble требует объект дерева, а не массив.

Неправильно:

const data = []

Правильно:

const data = {
    name: 'root',
    children: []
}

Подписи выходят за границы

Решения:

labelSkipRadius={20}

или:

enableLabels={false}

Архитектура Packed Circles

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

  1. вычисляется радиус каждого узла;
  2. строится иерархия;
  3. дочерние круги размещаются внутри родителя;
  4. система минимизирует пустое пространство;
  5. итоговая структура масштабируется под контейнер.

Внутри Nivo используется функциональность D3 Hierarchy и D3 Pack Layout.


Сравнение Bubble и Treemap

Bubble Treemap
Использует круги Использует прямоугольники
Лучше визуально Лучше использует пространство
Подходит для презентаций Подходит для аналитики
Менее точен визуально Более точен по площади
Высокая декоративность Высокая информативность

Когда использовать Bubble

Bubble подходит для:

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

Когда Bubble использовать не стоит

Bubble менее эффективен:

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

Интеграция с React

Типичный компонент:

import { ResponsiveBubble } from '@nivo/circle-packing'

export default function Chart({ data }) {
    return (
        <div style={{ height: 700 }}>
            <ResponsiveBubble
                data={data}
                identity="name"
                value="value"
                padding={4}
                colors={{ scheme: 'paired' }}
            />
        </div>
    )
}

TypeScript

Типизация данных:

type BubbleNode = {
    name: string
    value?: number
    children?: BubbleNode[]
}

Пример:

const data: BubbleNode = {
    name: 'root',
    children: [
        {
            name: 'React',
            value: 50
        }
    ]
}

Экспорт графика

SVG-версию можно экспортировать:

  • через html-to-image;
  • через dom-to-image;
  • через canvas;
  • через серверный рендеринг SVG.

Пример:

import { toPng } from 'html-to-image'

toPng(document.getElementById('chart'))
    .then((dataUrl) => {
        console.log(dataUrl)
    })

Server-Side Rendering

Nivo поддерживает SSR.

Особенности:

  • SVG работает корректно;
  • анимации рекомендуется отключать;
  • размеры контейнера должны быть определены заранее.
animate={false}

Комбинирование с другими графиками

Bubble часто используют вместе с:

  • Treemap;
  • Sunburst;
  • Bar Chart;
  • Pie Chart;
  • Sankey;
  • Network Graph.

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