Imagine you have a company budget dashboard with 500+ expense items. You spend 20 minutes hunting for the 'hole.' Our treemap shows it in 2 seconds. A treemap does one thing well: shows the proportion of parts in a hierarchical structure. The area of each rectangle is proportional to a value, color encodes an additional dimension — growth/decline, category, status. If you need to understand what takes the most space in a budget, category tree, file system, or asset portfolio, a treemap is read faster than any table. Development on React 18 and D3.js v7 gives full control over each pixel: custom tooltip, animation on drill-down, responsive layout for any resolution. Even with 10,000 nodes, the layout builds in 30–50 milliseconds — sufficient for real-time updates. Starting from $1,500 for a basic treemap, this solution typically saves 30% of analysis time.
What problems does a treemap solve?
Poor readability of tables with hierarchy. When a table has 10,000 rows, finding the top 5 categories requires scrolling and sorting. A treemap makes them visually apparent — the brain processes area instantly.
Difficulty perceiving proportions. Tables have numbers, but to understand how much the marketing budget outweighs the development budget, you need to do division. A treemap gives the answer without calculation.
Need to compare shares in real time. When data updates (e.g., stock quotes), a treemap with color-encoded changes shows which assets are rising and which are falling — at a glance.
How we implement a treemap with React and D3.js
We use React 18 and D3.js v7. D3 is not a chart library but a Swiss Army knife for working with data and SVG. It gives full control over every rectangle. We don't use high-level wrappers because each project requires custom solutions: custom tooltip, drill-down animation, responsive layout. The core of our hierarchy visualization is a custom React component that integrates seamlessly with your data.
Here is a typical treemap component implementation:
import { useEffect, useRef } from 'react';
import * as d3 from 'd3';
interface TreeNode {
name: string;
value?: number;
children?: TreeNode[];
change?: number; // % change for color encoding
}
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();
// Hierarchy
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');
// All nodes with children (for groups)
const leaves = root.leaves();
const ancestors = root.descendants().filter(d => d.depth === 1);
// Background rectangles for groups
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);
// Group labels
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);
// Leaves
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/>Change: ${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');
});
// Text inside cells (only if enough space)
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;
// Truncate text if it doesn't fit
let text = d.data.name;
while (this.getComputedTextLength() > maxWidth && text.length > 3) {
text = text.slice(0, -1);
el.text(text + '…');
}
});
// Value under the name
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' }} />;
}
Implementation details
In the code above, we create a hierarchy via d3.hierarchy, configure the layout with padding, render background rectangles for groups, labels, cells with tooltip and text. Responsive behavior ensures readability on any screen.How to add drill-down?
Drill-down is a key feature for interactive dashboards. The user clicks a group — and the treemap redraws with child-level data. We implemented a DrilldownTreemap component with breadcrumbs for navigation back.
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>
);
}
Which layout algorithm to choose?
D3.js provides four tiling algorithms. Comparison:
| Algorithm | Feature | When to use |
|---|---|---|
| treemapSquarify | Cells as square as possible, good aspect ratio | Default — best readability |
| treemapSliceDice | Alternates horizontal and vertical slices | When data has strict two-directional hierarchy |
| treemapSlice | Only horizontal slices | To display shares in one row |
| treemapResquarify | Reuses previous layout, minimizing movement | For animated data updates |
We always use treemapSquarify in production — it gives the best aspect ratio. If smooth data changes are needed (e.g., stock updates), we switch to treemapResquarify. As noted in the D3.js documentation, the Squarify algorithm provides optimal aspect ratio for most cases.
How to prepare data for a treemap?
For the treemap to work, client-side data must be transformed into a hierarchy. Often the API returns a flat list of rows. The buildHierarchy function groups them by categories and subcategories:
// Transform flat data into hierarchy
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;
}
Work process and timing
- Analysis — study the data structure, define hierarchy levels, metrics for area and color. (1 day)
- Design — create a prototype based on your data, agree on design and interactivity. (1 day)
- Implementation — write the component in React + D3.js, configure tooltip, drill-down, animation. (2–3 days)
- Testing — test on real data: large volumes, real-time updates. (1 day)
- Deployment — integrate into your project, provide usage instructions. (1 day)
| Deliverable | Timeline |
|---|---|
| Basic treemap with tooltip and drill-down | 2–3 days |
| With transition animation and color encoding | 4–6 days |
| Full cycle (analysis to deployment) | from 1 week |
Common mistakes in treemap development
- Ignoring paddingTop. Group headers overlap cells — text becomes unreadable. We always set paddingTop: 18.
- Text in too small cells. We check width and height before rendering, otherwise — overflow.
- Missing handling of empty data. If a node has value = 0, it still occupies space. Use filtering.
- Ignoring treemapResquarify for animation. Without it, every re-render shuffles cells — users lose context.
What's included in the work and why choose us
- Source code of the component (React + TypeScript + D3)
- Integration with your API
- Documentation for setup and customization
- Deployment instructions
- Team training (up to 2 hours)
- 3-month warranty for uninterrupted operation
We have 5+ years of experience in data visualization on complex interfaces and 30+ completed projects with treemaps, sunbursts, and other hierarchical diagrams. Our developers are certified in React and D3.js, and all components are built from scratch, with no licensing risks. Contact us for a consultation and order a treemap that speaks for itself. Get a free project estimate within 1 business day — just send us sample data and interactivity requirements.







