Представьте: у вас дашборд с бюджетом компании на 500+ статей расходов. Вы тратите 20 минут на поиск, где «дыра». Наш treemap показывает это за 2 секунды. Treemap делает одну вещь хорошо: показывает соотношение частей в иерархической структуре. Площадь прямоугольника пропорциональна значению, цвет — дополнительное измерение: рост/падение, категория, статус. Если нужно понять, что занимает больше всего места в бюджете, дереве категорий, файловой системе или портфеле активов, — treemap читается быстрее любой таблицы. При этом разработка на React 18 и D3.js v7 даёт полный контроль над каждым пикселем: кастомный tooltip, анимация при drill-down, адаптивная вёрстка под любое разрешение. Даже на 10 000 узлах layout строится за десятки миллисекунд — достаточно для real-time обновлений.
Какие задачи решает treemap-диаграмма?
Плохая читаемость таблиц с иерархией. Когда в таблице 10 000 строк, найти топ-5 категорий можно только скроллом и сортировкой. Treemap показывает их визуально — мозг обрабатывает площадь мгновенно.
Сложность восприятия пропорций. В таблице числа, но чтобы понять, насколько бюджет маркетинга больше бюджета разработки, нужно делить. Treemap даёт ответ без вычислений.
Необходимость сравнивать доли в реальном времени. При обновлении данных (например, котировки акций) treemap с цветовым кодированием изменений показывает, какие активы растут, а какие падают. Достаточно взгляда.
Как мы реализуем treemap на React и D3.js?
Мы используем React 18 и D3.js v7. D3 — это не библиотека диаграмм, а швейцарский нож для работы с данными и SVG. Он даёт полный контроль над каждым прямоугольником. Мы не используем high-level обёртки, потому что каждый проект требует кастомных решений: особый tooltip, анимация при drill-down, адаптивная вёрстка.
Вот типичная реализация компонента treemap:
import { useEffect, useRef } from 'react';
import * as d3 from 'd3';
interface TreeNode {
name: string;
value?: number;
children?: TreeNode[];
change?: number; // % изменения для цветового кодирования
}
interface TreemapProps {
data: TreeNode;
width?: number;
height?: number;
colorBy?: 'category' | 'change';
}
export function Treemap({ data, width = 800, height = 500, colorBy = 'category' }: TreemapProps) {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (!svgRef.current) return;
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
// Иерархия
const root = d3.hierarchy(data)
.sum(d => d.value ?? 0)
.sort((a, b) => (b.value ?? 0) - (a.value ?? 0));
// Layout
d3.treemap()
.size([width, height])
.paddingOuter(3)
.paddingInner(2)
.paddingTop(18)
.round(true)(root);
const colorScale = colorBy === 'change'
? d3.scaleDiverging(d3.interpolateRdYlGn).domain([-30, 0, 30])
: d3.scaleOrdinal(d3.schemeTableau10);
const tooltip = d3.select('body').append('div')
.style('position', 'absolute')
.style('display', 'none')
.style('background', 'rgba(15,23,42,0.9)')
.style('color', '#f1f5f9')
.style('padding', '8px 12px')
.style('border-radius', '4px')
.style('font-size', '13px')
.style('pointer-events', 'none')
.style('max-width', '220px');
// Все узлы с потомками (для группировок)
const leaves = root.leaves();
const ancestors = root.descendants().filter(d => d.depth === 1);
// Фоновые прямоугольники для групп
svg.selectAll('.group-rect')
.data(ancestors)
.join('rect')
.attr('class', 'group-rect')
.attr('x', (d: any) => d.x0)
.attr('y', (d: any) => d.y0)
.attr('width', (d: any) => d.x1 - d.x0)
.attr('height', (d: any) => d.y1 - d.y0)
.attr('fill', (d: any) => colorBy === 'change' ? '#e2e8f0' : d3.color(colorScale(d.data.name))!.brighter(0.7).toString())
.attr('stroke', '#fff')
.attr('stroke-width', 2);
// Заголовки групп
svg.selectAll('.group-label')
.data(ancestors)
.join('text')
.attr('class', 'group-label')
.attr('x', (d: any) => d.x0 + 6)
.attr('y', (d: any) => d.y0 + 13)
.attr('font-size', 11)
.attr('font-weight', '600')
.attr('fill', '#374151')
.text((d: any) => d.data.name);
// Листья
const cell = svg.selectAll('.cell')
.data(leaves)
.join('g')
.attr('class', 'cell')
.attr('transform', (d: any) => `translate(${d.x0},${d.y0})`);
cell.append('rect')
.attr('width', (d: any) => d.x1 - d.x0)
.attr('height', (d: any) => d.y1 - d.y0)
.attr('fill', (d: any) => {
if (colorBy === 'change') return colorScale(d.data.change ?? 0);
return colorScale((d.parent?.data.name ?? '') as string);
})
.attr('fill-opacity', 0.85)
.attr('stroke', '#fff')
.attr('stroke-width', 1)
.on('mouseover', (event, d: any) => {
d3.select(event.currentTarget).attr('fill-opacity', 1);
const pct = d.data.change != null
? `<br/>Изменение: ${d.data.change > 0 ? '+' : ''}${d.data.change.toFixed(1)}%`
: '';
tooltip
.style('display', 'block')
.style('left', `${event.pageX + 12}px`)
.style('top', `${event.pageY - 28}px`)
.html(`<strong>${d.data.name}</strong><br/>${d3.format(',.0f')(d.value ?? 0)}${pct}`);
})
.on('mouseout', (event) => {
d3.select(event.currentTarget).attr('fill-opacity', 0.85);
tooltip.style('display', 'none');
});
// Текст внутри ячеек (только если достаточно места)
cell.append('text')
.attr('x', 4)
.attr('y', 14)
.attr('font-size', 11)
.attr('fill', '#fff')
.attr('font-weight', '500')
.text((d: any) => {
const w = d.x1 - d.x0;
const h = d.y1 - d.y0;
return w > 40 && h > 20 ? d.data.name : '';
})
.each(function(d: any) {
const el = d3.select(this);
const maxWidth = d.x1 - d.x0 - 8;
// Обрезаем текст если не помещается
let text = d.data.name;
while (this.getComputedTextLength() > maxWidth && text.length > 3) {
text = text.slice(0, -1);
el.text(text + '…');
}
});
// Значение под названием
cell.append('text')
.attr('x', 4)
.attr('y', 26)
.attr('font-size', 10)
.attr('fill', 'rgba(255,255,255,0.8)')
.text((d: any) => {
const w = d.x1 - d.x0;
const h = d.y1 - d.y0;
return w > 50 && h > 35 ? d3.format(',.0f')(d.value ?? 0) : '';
});
return () => { tooltip.remove(); };
}, [data, width, height, colorBy]);
return <svg ref={svgRef} width={width} height={height} style={{ display: 'block' }} />;
}
Описание реализации
В коде выше мы создаём иерархию через d3.hierarchy, настраиваем layout с отступами, рендерим фоновые прямоугольники для групп, заголовки, ячейки с tooltip и текстом. Адаптивное поведение гарантирует читаемость на любом экране.Как добавить drill-down?
Drill-down — ключевая функция для интерактивных дашбордов. Пользователь кликает по группе — и treemap перерисовывается с данными дочернего уровня. Мы реализовали компонент DrilldownTreemap с хлебными крошками для навигации назад.
function DrilldownTreemap({ data }: { data: TreeNode }) {
const [currentNode, setCurrentNode] = useState<TreeNode>(data);
const [breadcrumb, setBreadcrumb] = useState<TreeNode[]>([data]);
function drillDown(node: TreeNode) {
if (!node.children?.length) return;
setCurrentNode(node);
setBreadcrumb(prev => [...prev, node]);
}
function drillUp(index: number) {
const target = breadcrumb[index];
setCurrentNode(target);
setBreadcrumb(prev => prev.slice(0, index + 1));
}
return (
<div>
<nav className="flex gap-2 text-sm mb-3">
{breadcrumb.map((node, i) => (
<span key={i}>
{i > 0 && <span className="text-gray-400 mx-1">/</span>}
<button
onClick={() => drillUp(i)}
className={i === breadcrumb.length - 1 ? 'font-semibold' : 'text-blue-600 hover:underline'}
>
{node.name}
</button>
</span>
))}
</nav>
<Treemap data={currentNode} onCellClick={drillDown} />
</div>
);
}
Какой алгоритм раскладки выбрать?
D3.js предоставляет четыре тайлинг-алгоритма. Сравнение:
| Алгоритм | Особенность | Когда использовать |
|---|---|---|
treemapSquarify |
Ячейки максимально квадратные, хороший aspect ratio | По умолчанию — лучшая читаемость |
treemapSliceDice |
Чередует горизонтальные и вертикальные срезы | Когда данные имеют строгую иерархию по двум направлениям |
treemapSlice |
Только горизонтальные срезы | Для отображения долей в одной строке |
treemapResquarify |
Переиспользует прошлую раскладку, минимизируя перемещения | Для анимированного обновления данных |
Мы всегда используем treemapSquarify в production — он даёт наилучшее соотношение сторон. Если необходима плавная смена данных (например, при обновлении котировок), переключаемся на treemapResquarify. Как отмечается в документации D3.js Hierarchy, алгоритм Squarify обеспечивает оптимальный aspect ratio для большинства случаев.
Как подготовить данные для treemap?
Чтобы treemap работал, данные на клиенте должны быть преобразованы в иерархию. Часто API возвращает плоский список строк. Функция buildHierarchy группирует их по категориям и подкатегориям:
// Трансформация плоских данных в иерархию
function buildHierarchy(items: { category: string; subcategory: string; name: string; value: number }[]): TreeNode {
const root: TreeNode = { name: 'root', children: [] };
items.forEach(item => {
let cat = root.children!.find(c => c.name === item.category);
if (!cat) {
cat = { name: item.category, children: [] };
root.children!.push(cat);
}
let subcat = cat.children!.find(c => c.name === item.subcategory);
if (!subcat) {
subcat = { name: item.subcategory, children: [] };
cat.children!.push(subcat);
}
subcat.children!.push({ name: item.name, value: item.value });
});
return root;
}
Процесс работы и сроки
- Аналитика — изучаем структуру данных, определяем уровни иерархии, метрики для площади и цвета. (1 день)
- Проектирование — создаём прототип на основе ваших данных, согласовываем дизайн и интерактив. (1 день)
- Реализация — пишем компонент на React + D3.js, настраиваем tooltip, drill-down, анимацию. (2-3 дня)
- Тестирование — проверяем на реальных данных: большие объёмы, обновления в реальном времени. (1 день)
- Деплой — интегрируем в ваш проект, даём инструкцию по использованию. (1 день)
| Готовность | Срок |
|---|---|
| Базовый treemap с tooltip и drill-down | 2–3 дня |
| С анимацией переходов и цветовым кодированием | 4–6 дней |
| Полный цикл (аналитика → деплой) | от 1 недели |
Какие ошибки допускают при разработке treemap?
- Игнорирование paddingTop. Заголовки групп накладываются на ячейки — текст нечитаем. Мы всегда задаём
paddingTop: 18. - Текст в слишком маленьких ячейках. Проверяем ширину и высоту перед рендером, иначе — переполнение.
- Отсутствие обработки пустых данных. Если узел с
value = 0, он всё равно занимает место. Используем фильтрацию. - Игнорирование
treemapResquarifyпри анимации. Без него каждый ререндер перемешивает ячейки — пользователь теряет контекст.
Что входит в работу и почему выбирают нас
- Исходный код компонента (React + TypeScript + D3)
- Интеграция с вашим API
- Документация по настройке и кастомизации
- Инструкция по развёртыванию
- Обучение команды (до 2 часов)
- Гарантия 3 месяца на бесперебойную работу
Мы имеем 5+ лет опыта в визуализации данных на сложных интерфейсах и 30+ реализованных проектов с treemap, sunburst и другими иерархическими диаграммами. Наши разработчики сертифицированы по React и D3.js, а все компоненты создаются с нуля, без рисков лицензирования. Свяжитесь с нами для консультации и закажите разработку treemap, который заговорит сам за себя. Получите бесплатную оценку вашего проекта за 1 рабочий день — просто пришлите пример данных и требования по интерактиву.







