How to Build a Responsive Rectangular Treemap with Squarified Layout in JavaScript
This article explains how to implement a rectangular treemap for outpatient disease data using JavaScript, covering layout algorithms such as Slice‑and‑Dice and Squarified, color filling, and canvas event handling with detailed code examples and performance evaluation metrics.
1. Background
In many work and life scenarios we encounter rectangular treemaps; the author needed to draw a treemap of outpatient disease data. Although libraries like echart and d3 provide implementations, the author explored the underlying principles, focusing on layout algorithms, color filling, and the canvas event system.
The data set is assumed to be sorted with a total value of 100:
<code>const data = [
{name: '疾病1', value: 36},
{name: '疾病2', value: 30},
{name: '疾病3', value: 23},
{name: '疾病4', value: 8},
{name: '疾病5', value: 2},
{name: '疾病6', value: 1}
];
</code>2. Layout Algorithm
2.1 Slice and Dice Algorithm
The simplest approach fills rectangles from left to right based solely on area proportion, but this yields poor visual balance and makes small right‑most rectangles hard to select.
2.2 Squarified Algorithm
To improve aspect ratios, the Squarified algorithm (Bruls 1999) sorts nodes by weight, places them along the shortest side, and chooses between extending the current row or starting a new one based on the worst aspect ratio.
Sort child nodes by weight descending.
Place each node along the shortest side using left‑to‑right or top‑to‑bottom filling.
When adding a node, compare the worst aspect ratio of the current row with that of a new row and choose the better.
The core recursive implementation is:
<code>/**
* @param {Array} children Array of rectangle areas to layout
* @param {Array} row Current row of rectangles
* @param {Number} minSide Shorter side of the remaining container
*/
const squarify = (children, row, minSide) => {
if (children.length === 1) {
return layoutLastRow(row, children, minSide);
}
const rowWithChild = [...row, children[0]];
if (row.length === 0 || worstRatio(row, minSide) >= worstRatio(rowWithChild, minSide)) {
children.shift();
return squarify(children, rowWithChild, minSide);
} else {
layoutRow(row, minSide, getMinSide().vertical);
return squarify(children, [], getMinSide().value);
}
};
function worstRatio(row, minSide) {
const sum = row.reduce(sumReducer, 0);
const rowMax = getMaximum(row);
const rowMin = getMinimum(row);
return Math.max((minSide ** 2 * rowMax) / (sum ** 2), (sum ** 2) / (minSide ** 2 * rowMin));
}
const Rectangle = {data: [], xBeginning: 0, yBeginning: 0, totalWidth: canvasWidth, totalHeight: canvasHeight};
const getMinSide = () => {
if (Rectangle.totalHeight > Rectangle.totalWidth) {
return {value: Rectangle.totalWidth, vertical: false};
}
return {value: Rectangle.totalHeight, vertical: true};
};
const layoutRow = (row, height, vertical) => {
const rowWidth = row.reduce(sumReducer, 0) / height;
row.forEach(rowItem => {
const rowHeight = rowItem / rowWidth;
const {xBeginning} = Rectangle;
const {yBeginning} = Rectangle;
const data = {
x: xBeginning,
y: yBeginning,
width: rowWidth,
height: rowHeight
};
Rectangle.yBeginning += rowHeight;
Rectangle.data.push(data);
});
if (vertical) {
Rectangle.xBeginning += rowWidth;
Rectangle.yBeginning -= height;
Rectangle.totalWidth -= rowWidth;
}
};
</code>The drawing process is illustrated below:
2.3 Other Algorithms
Pivotand
Stripalgorithms improve stability by balancing aspect ratio and handling data changes.
BinaryTreelayout recursively splits nodes into a balanced binary tree, using horizontal division for wide rectangles and vertical for tall ones.
2.4 Evaluation Metrics
The layouts are evaluated by average aspect ratio (AAR), stability, and continuity.
3. Color Filling
A simple fixed color palette can be used:
<code>const colors = ['#c23531', '#2f4554', '#61a0a8', '#d48265', '#91c7ae', '#749f83', '#ca8622'];
</code>4. Canvas Event System
To show tooltips on hover,
isPointInPathand
Path2Dare used to detect mouse position within each rectangle:
<code>interface diseaseInfo {name: string; value: number;}
interface rectInfo {x: number; y: number; width: number; height: number; data: diseaseInfo;}
rects.forEach(item => {
const path = new Path2D();
path.rect(item.x, item.y, item.width, item.height);
ctx.fill(path);
item.path = path;
});
const canvasInfo = canvas.getBoundingClientRect();
canvas.addEventListener('mousemove', e => {
result.forEach(item => {
if (ctx.isPointInPath(item.path, e.clientX - canvasInfo.left, e.clientY - canvasInfo.top)) {
showTips(e.clientX, e.clientY, item.data);
}
});
});
</code>An alternative uses an off‑screen canvas where each rectangle is drawn with a unique RGBA id; the mouse coordinates are mapped to that color via
getImageDatato identify the rectangle.
Ray casting method
Angle summation method (for convex polygons)
Most mature visualization frameworks provide a publish‑subscribe event system to support such interactions.
The final effect looks like the following:
5. References
https://www.win.tue.nl/~vanwijk/stm.pdf
https://zhuanlan.zhihu.com/p/57873460
https://juejin.cn/post/6888209975965909000#heading-4
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.