Компонент Bubble в библиотеке Nivo предназначен для визуализации иерархических структур данных в виде вложенных окружностей. Такой подход особенно полезен при отображении:
Каждый круг представляет отдельный узел дерева. Размер пузыря определяется числовым значением, а вложенность отражает иерархию объектов.
Для работы с Bubble Chart используется отдельный пакет:
npm install @nivo/circle-packing
При использовании React также требуется базовая инфраструктура:
npm install react react-dom
Компонент использует древовидный формат данных.
Минимальная структура:
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 |
Числовое значение листового узла |
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.
Компонент автоматически:
Определяет поле, используемое как идентификатор узла.
identity="name"
Допустимы:
identity="id"
identity="title"
identity={node => node.name}
Указывает поле, отвечающее за размер пузыря.
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={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={2}
borderColor={{
from: 'color',
modifiers: [['darker', 0.5]]
}}
label="name"
Скрывает подписи у маленьких окружностей.
labelSkipRadius={12}
labelTextColor={{
from: 'color',
modifiers: [['darker', 3]]
}}
valueFormat=">-.2f"
Пример:
<ResponsiveBubble
valueFormat={value => `${value} users`}
/>
Компонент поддерживает анимированные переходы.
animate={true}
motionConfig="gentle"
Варианты:
motionConfig="default"
motionConfig="wobbly"
motionConfig="stiff"
motionConfig="slow"
Bubble Chart поддерживает:
onCl ick={(node) => {
console.log(node)
}}
Пример:
<ResponsiveBubble
onCl ick={(node) => {
alert(node.data.name)
}}
/>
onMouseEn ter={(node) => {
console.log('hover', node)
}}
onMouseLe ave={(node) => {
console.log('leave')
}}
tooltip={({ node }) => (
<div
style={{
padding: 12,
background: '#fff',
border: '1px solid #ccc'
}}
>
<strong>{node.data.name}</strong>
<div>Value: {node.value}</div>
</div>
)}
Свойство depth показывает уровень вложенности.
colors={node => {
switch(node.depth) {
case 1:
return '#2563eb'
case 2:
return '#16a34a'
case 3:
return '#dc2626'
default:
return '#6b7280'
}
}}
childColor={{
from: 'color',
modifiers: [['brighter', 0.4]]
}}
leavesOnly={true}
В этом режиме отображаются только конечные элементы дерева.
sortByValue={true}
Сортировка улучшает распределение пузырей.
theme={{
labels: {
text: {
fontSize: 14,
fontWeight: 700
}
},
tooltip: {
container: {
background: '#111827',
color: '#ffffff'
}
}
}}
Nivo предоставляет две реализации:
| Компонент | Особенности |
|---|---|
ResponsiveBubble |
SVG, высокая кастомизация |
ResponsiveBubbleHtml |
HTML rendering |
ResponsiveBubbleCanvas |
высокая производительность |
Для больших объёмов данных рекомендуется Canvas-версия.
import { ResponsiveBubbleCanvas } from '@nivo/circle-packing'
Пример:
<ResponsiveBubbleCanvas
data={data}
identity="name"
value="value"
/>
Canvas значительно снижает нагрузку на DOM.
При работе с крупными деревьями полезно:
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)
}))
}
Типизация дерева:
type BubbleNode = {
name: string
value?: number
children?: BubbleNode[]
}
Пример:
const data: BubbleNode = {
name: 'root',
children: []
}
const data = useSelector(state => state.analytics.tree)
Для корректной работы SSR часто применяется динамический импорт.
import dynamic from 'next/dynamic'
const ResponsiveBubble = dynamic(
() =>
import('@nivo/circle-packing').then(
mod => mod.ResponsiveBubble
),
{
ssr: false
}
)
const chartData = useMemo(() => prepareData(data), [data])
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: []
Листовые элементы должны содержать числовое значение.
{
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 особенно эффективен при:
Следует учитывать:
| Bubble | Treemap |
|---|---|
| Более наглядная иерархия | Более эффективное использование пространства |
| Красивое органичное распределение | Точное сравнение площадей |
| Подходит для презентаций | Подходит для аналитики |
| Хуже масштабируется | Лучше работает с большими данными |
Компонент строится поверх:
Внутри используется алгоритм упаковки окружностей, минимизирующий пустое пространство и предотвращающий пересечения пузырей.