Frontend Development 11 min read

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.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
How to Build a Responsive Rectangular Treemap with Squarified Layout in JavaScript

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

Pivot

and

Strip

algorithms improve stability by balancing aspect ratio and handling data changes.

BinaryTree

layout 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,

isPointInPath

and

Path2D

are 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

getImageData

to 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

frontendJavaScriptCanvaslayout algorithmVisualizationtreemap
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.