Иерархические данные для Bubble

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

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

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


Установка зависимостей

Для работы с Bubble Chart используется отдельный пакет:

npm install @nivo/circle-packing

При использовании React также требуется базовая инфраструктура:

npm install react react-dom

Структура данных для Bubble

Компонент использует древовидный формат данных.

Минимальная структура:

const data = {
    name: 'root',
    children: [
        {
            name: 'Frontend',
            children: [
                {
                    name: 'React',
                    value: 120
                },
                {
                    name: 'Vue',
                    value: 80
                }
            ]
        },
        {
            name: 'Backend',
            children: [
                {
                    name: 'Node.js',
                    value: 140
                },
                {
                    name: 'Python',
                    value: 100
                }
            ]
        }
    ]
}

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

Свойство Назначение
name Название узла
children Массив дочерних элементов
value Числовое значение листового узла

Базовый пример Bubble Chart

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

const data = {
    name: 'root',
    children: [
        {
            name: 'Frontend',
            children: [
                {
                    name: 'React',
                    value: 120
                },
                {
                    name: 'Vue',
                    value: 80
                }
            ]
        },
        {
            name: 'Backend',
            children: [
                {
                    name: 'Node.js',
                    value: 140
                },
                {
                    name: 'Python',
                    value: 100
                }
            ]
        }
    ]
}

export default function App() {
    return (
        <div style={{ height: 600 }}>
            <ResponsiveBubble
                data={data}
                identity="name"
                value="value"
                padding={6}
                colors={{ scheme: 'nivo' }}
                borderWidth={2}
                borderColor={{ from: 'color' }}
                labelSkipRadius={10}
                labelTextColor={{
                    from: 'color',
                    modifiers: [['darker', 2]]
                }}
            />
        </div>
    )
}

ResponsiveBubble

Наиболее распространённым вариантом является ResponsiveBubble.

Компонент автоматически:

  • адаптируется под размеры контейнера;
  • пересчитывает геометрию пузырей;
  • поддерживает responsive-подход.

Свойство identity

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

identity="name"

Допустимы:

identity="id"
identity="title"
identity={node => node.name}

Свойство value

Указывает поле, отвечающее за размер пузыря.

value="value"

Пример:

{
    name: 'React',
    value: 120
}

Чем больше значение — тем крупнее окружность.


Вложенность и иерархия

Bubble Chart строится на основе рекурсивной структуры.

Пример более глубокой вложенности:

const data = {
    name: 'Company',
    children: [
        {
            name: 'Development',
            children: [
                {
                    name: 'Frontend',
                    children: [
                        {
                            name: 'React',
                            value: 150
                        },
                        {
                            name: 'Angular',
                            value: 90
                        }
                    ]
                },
                {
                    name: 'Backend',
                    children: [
                        {
                            name: 'Node.js',
                            value: 180
                        }
                    ]
                }
            ]
        }
    ]
}

Каждый уровень дерева формирует новый уровень вложенных окружностей.


Настройка отступов между пузырями

padding

padding={4}

Пример:

<ResponsiveBubble
    padding={8}
/>

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


Управление цветами

Цветовая схема

colors={{ scheme: 'category10' }}

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

colors={{ scheme: 'accent' }}
colors={{ scheme: 'dark2' }}
colors={{ scheme: 'paired' }}
colors={{ scheme: 'set3' }}

Использование собственных цветов

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

    return '#f59e0b'
}}

Границы пузырей

borderWidth

borderWidth={2}

borderColor

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

Работа с подписями

label

label="name"

labelSkipRadius

Скрывает подписи у маленьких окружностей.

labelSkipRadius={12}

Цвет текста

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

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

valueFormat

valueFormat=">-.2f"

Пример:

<ResponsiveBubble
    valueFormat={value => `${value} users`}
/>

Анимация

Компонент поддерживает анимированные переходы.

animate={true}
motionConfig="gentle"

Варианты:

motionConfig="default"
motionConfig="wobbly"
motionConfig="stiff"
motionConfig="slow"

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

Bubble Chart поддерживает:

  • hover-состояния;
  • tooltips;
  • обработчики событий;
  • выделение элементов.

Обработка кликов

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

Пример:

<ResponsiveBubble
    onCl ick={(node) => {
        alert(node.data.name)
    }}
/>

Hover-состояния

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

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

Настройка tooltip

tooltip={({ node }) => (
    <div
        style={{
            padding: 12,
            background: '#fff',
            border: '1px solid #ccc'
        }}
    >
        <strong>{node.data.name}</strong>
        <div>Value: {node.value}</div>
    </div>
)}

Работа с depth

Свойство depth показывает уровень вложенности.

colors={node => {
    switch(node.depth) {
        case 1:
            return '#2563eb'

        case 2:
            return '#16a34a'

        case 3:
            return '#dc2626'

        default:
            return '#6b7280'
    }
}}

Фильтрация узлов

childColor

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

Отключение корневого узла

leavesOnly

leavesOnly={true}

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


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

sortByValue

sortByValue={true}

Сортировка улучшает распределение пузырей.


Настройка темы

theme={{
    labels: {
        text: {
            fontSize: 14,
            fontWeight: 700
        }
    },
    tooltip: {
        container: {
            background: '#111827',
            color: '#ffffff'
        }
    }
}}

SVG и Canvas версии

Nivo предоставляет две реализации:

Компонент Особенности
ResponsiveBubble SVG, высокая кастомизация
ResponsiveBubbleHtml HTML rendering
ResponsiveBubbleCanvas высокая производительность

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

Для больших объёмов данных рекомендуется Canvas-версия.

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

Пример:

<ResponsiveBubbleCanvas
    data={data}
    identity="name"
    value="value"
/>

Canvas значительно снижает нагрузку на DOM.


Большие наборы данных

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

  • отключать labels;
  • уменьшать глубину отображения;
  • использовать Canvas;
  • ограничивать tooltip;
  • минимизировать re-render.

Ограничение глубины отображения

inheritColorFromParent

inheritColorFromParent={true}

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

Bubble Chart удобно работает с API.

Пример:

const [data, setData] = useState(null)

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

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

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

const memoizedData = useMemo(() => {
    return buildTree(items)
}, [items])

Генерация дерева из плоского массива

Частая задача — преобразование обычного списка в иерархию.

Исходный массив:

const items = [
    {
        id: 1,
        parent: null,
        name: 'Frontend'
    },
    {
        id: 2,
        parent: 1,
        name: 'React',
        value: 100
    }
]

Построение дерева:

function buildTree(items, parent = null) {
    return items
        .filter(item => item.parent === parent)
        .map(item => ({
            ...item,
            children: buildTree(items, item.id)
        }))
}

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

Типизация дерева:

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

Пример:

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

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

const data = useSelector(state => state.analytics.tree)

Интеграция с Next.js

Для корректной работы SSR часто применяется динамический импорт.

import dynamic from 'next/dynamic'

const ResponsiveBubble = dynamic(
    () =>
        import('@nivo/circle-packing').then(
            mod => mod.ResponsiveBubble
        ),
    {
        ssr: false
    }
)

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

useMemo

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

React.memo

export default React.memo(BubbleChart)

Минимизация пересоздания объектов

Плохо:

<ResponsiveBubble
    theme={{
        fontSize: 12
    }}
/>

Лучше:

const theme = useMemo(() => ({
    fontSize: 12
}), [])

Частые ошибки

Отсутствие корневого объекта

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

[
    {
        name: 'React',
        value: 100
    }
]

Правильно:

{
    name: 'root',
    children: [
        {
            name: 'React',
            value: 100
        }
    ]
}

Ошибки структуры children

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

children: {}

Правильно:

children: []

Проблемы с value

Листовые элементы должны содержать числовое значение.

{
    name: 'React',
    value: 120
}

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

const data = {
    name: 'Traffic',
    children: [
        {
            name: 'Social',
            children: [
                {
                    name: 'Twitter',
                    value: 240
                },
                {
                    name: 'Facebook',
                    value: 180
                }
            ]
        },
        {
            name: 'Search',
            children: [
                {
                    name: 'Google',
                    value: 320
                },
                {
                    name: 'Bing',
                    value: 90
                }
            ]
        }
    ]
}

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

<ResponsiveBubble
    data={data}
    identity="name"
    value="value"
    padding={5}
    colors={{ scheme: 'spectral' }}
    borderWidth={1}
    animate={true}
    motionConfig="gentle"
    labelSkipRadius={14}
    sortByValue={true}
/>

Когда Bubble Chart подходит лучше других диаграмм

Bubble особенно эффективен при:

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

Ограничения Bubble Chart

Следует учитывать:

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

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

Bubble Treemap
Более наглядная иерархия Более эффективное использование пространства
Красивое органичное распределение Точное сравнение площадей
Подходит для презентаций Подходит для аналитики
Хуже масштабируется Лучше работает с большими данными

Архитектура Bubble Chart в Nivo

Компонент строится поверх:

  • React;
  • D3 hierarchy;
  • circle packing algorithms;
  • SVG или Canvas rendering.

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