Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A squarified treemap can show a hierarchy as nested rectangles whose areas match quantitative values, while a separate interaction layer lets users drill into any group and return to its parent. Building both pieces yourself does not fix a universal limitation in charting packages—D3 provides a treemap layout API, and other applications support drill-down—but it gives you precise control over packing, navigation, focus, and edge cases.

This guide explains the geometry, gives a hand-built JavaScript implementation, and then shows how to add drill-down behavior or use D3 when you want a maintained layout engine.

What a squarified treemap actually does

A treemap converts hierarchical data into a rectangle containing rectangles. The outer rectangle represents the root. Each child receives an area proportional to its value, and that child rectangle can be subdivided again for its own children. A file system, for example, can use folders as internal nodes and files as leaves; a budget can use departments, projects, and line items.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The area comparison is local to the relevant parent. If a department occupies 40% of the company rectangle, its rectangle represents 40% of the company’s total value. Inside that department, its projects divide the department rectangle according to their own values. This recursive area encoding is the central treemap idea documented by D3’s treemap API.

#1 Best Overall
Sale
Storytelling with Data: A Data Visualization Guide for Business Professionals
  • Wiley
  • Language: english
  • Book - storytelling with data: a data visualization guide for business professionals

“Squarified” describes how siblings are packed. Instead of filling the parent with long, thin strips, the algorithm greedily builds rows whose rectangles have aspect ratios as close to one as the available area allows. Mark Bruls, Kees Huizing, and Jarke J. van Wijk introduced this approach in 2000; their paper notes that the standard method often produces thin, elongated rectangles that are difficult to compare and select, and proposes layouts that approximate squares. The publication record is available from the Eurographics Digital Library.

Layout and interaction are different jobs. The layout computes coordinates. Your application decides what a click, double-click, Enter key, or arrow key does, how the current path is displayed, and how a user returns to the root.

The geometry: values become areas, then coordinates

1. Aggregate every internal node

Leaves normally carry the raw measure, while groups derive their totals from descendants. A node with no positive value has no area to display. Decide whether that node should be hidden, shown in a separate “zero” list, or rendered with a minimum hit target; do not silently assign it a misleading area.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

2. Convert values to target areas

For a parent rectangle with width W and height H, the available area is W × H. If the positive child values sum to S, child i receives the target area:

areaᵢ = (valueᵢ / S) × W × H

The rectangle’s width and height must multiply to that area, allowing for small rounding errors at the rendering stage. Keep calculations in floating point and round only when drawing pixels; early rounding creates gaps or overlaps.

3. Sort before packing

Sorting children from largest to smallest generally gives the greedy algorithm a better chance to produce readable rows. Sorting is also useful for stable labels and keyboard order. If your product requires a domain order—months, workflow stages, or a ranked list—preserve that order deliberately and accept that the packing may be less compact.

4. Build a row greedily

Start with an empty row and the shorter side of the remaining parent rectangle as the reference length. Add the next child provisionally. Compute the row’s worst aspect ratio. Keep the child if the worst ratio improves or stays equal; otherwise, finalize the current row, place it, and start a new row with that child.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a row with total area A, largest item area maxA, smallest item area minA, and reference side s, a common worst-ratio measure is:

max((s² × maxA) / A², A² / (s² × minA))

A value near 1 is close to square. The algorithm is greedy: it improves the current row, not the global optimum. The final row often has a different shape because it must consume whatever area remains.

5. Lay out the row and recurse

If the remaining rectangle is wider than it is tall, a row can span its width and consume a strip of height rowArea / remainingWidth. Otherwise it spans the height and consumes a strip of width rowArea / remainingHeight. Within the strip, divide the long dimension in proportion to each item’s area. Recurse into every child using its assigned rectangle.

A hand-built squarified layout in JavaScript

The following implementation is intentionally explicit. It rejects negative values, ignores zero-area children for geometry, sorts positive children by value, and recursively assigns x, y, width, and height. It leaves interaction and rendering to the caller.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function squarifyTree(node, x, y, width, height) {
  if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) {
    throw new Error("Parent dimensions must be finite and non-negative");
  }

  const children = (node.children || []).map(child => {
    const value = Number(child.value);
    if (!Number.isFinite(value) || value < 0) {
      throw new Error(`Invalid value for ${child.id || "unnamed node"}`);
    }
    return { child, value };
  }).filter(item => item.value > 0)
    .sort((a, b) => b.value - a.value);

  const total = children.reduce((sum, item) => sum + item.value, 0);
  if (!total || !width || !height) return;

  const scale = (width * height) / total;
  const items = children.map(item => ({
    ...item,
    area: item.value * scale
  }));

  let remaining = { x, y, width, height };
  let row = [];

  const worst = (candidate, side) => {
    const area = candidate.reduce((s, item) => s + item.area, 0);
    const maxArea = Math.max(...candidate.map(item => item.area));
    const minArea = Math.min(...candidate.map(item => item.area));
    return Math.max(
      (side * side * maxArea) / (area * area),
      (area * area) / (side * side * minArea)
    );
  };

  const placeRow = placed => {
    const rowArea = placed.reduce((s, item) => s + item.area, 0);
    const horizontal = remaining.width >= remaining.height;
    const side = horizontal ? remaining.width : remaining.height;
    const thickness = rowArea / side;
    let cursor = horizontal ? remaining.x : remaining.y;

    for (const item of placed) {
      const length = item.area / thickness;
      item.child.x = horizontal ? cursor : remaining.x;
      item.child.y = horizontal ? remaining.y : cursor;
      item.child.width = horizontal ? length : thickness;
      item.child.height = horizontal ? thickness : length;
      cursor += length;
      squarifyTree(item.child, item.child.x, item.child.y,
                   item.child.width, item.child.height);
    }

    if (horizontal) {
      remaining.y += thickness;
      remaining.height -= thickness;
    } else {
      remaining.x += thickness;
      remaining.width -= thickness;
    }
  };

  for (const item of items) {
    const side = Math.min(remaining.width, remaining.height);
    const candidate = row.concat(item);
    if (!row.length || worst(candidate, side) <= worst(row, side)) {
      row.push(item);
    } else {
      placeRow(row);
      row = [item];
    }
  }
  if (row.length) placeRow(row);
}

This version uses the shorter remaining side when evaluating a candidate row and chooses the strip orientation from the remaining rectangle. Test it with known data before shipping: implementations differ in tie handling, padding, and the exact orientation rule, and those details affect the visual result.

Validation cases you should not skip

  • Negative values: reject them or transform them in a documented, domain-specific way. A negative quantity cannot be represented by a positive rectangle area.
  • Zeros and empty groups: exclude zero-area nodes from packing and decide how their labels appear. An empty parent should not recurse forever.
  • Very small values: enforce a minimum visual hit target only in the interaction layer; do not inflate the geometric area without showing that the display is not value-faithful.
  • Rounding: draw from floating-point coordinates, or assign the final remainder to the last rectangle in a row so neighboring edges meet.
  • Deep trees: use an iterative traversal or guard recursion depth if untrusted data can create thousands of levels.
  • Duplicate identifiers: give every node a stable unique key. Labels are not reliable keys for animation or focus state.

Adding drill-down without coupling it to the layout

Keep one immutable hierarchy and a separate focus pointer (or path of node IDs). Render only the focused node’s children inside the viewport, while retaining a breadcrumb trail to its ancestors. Recompute geometry when focus changes; do not mutate values merely because the user navigated.

  1. Render the current focus: call the layout with the focused node and the viewport rectangle. Draw a parent background, child rectangles, labels, and a visible title such as “All departments / Engineering.”
  2. Attach a deliberate activation rule: use a click for selection and a second action—double-click or Enter—for navigation if accidental drill-down would be costly. If single-click navigation is appropriate, provide a clear Back control.
  3. Store the previous focus: push the current node ID onto a history stack before entering a child. The breadcrumb can use the same stack.
  4. Return upward: Backspace, a breadcrumb button, or an on-screen Up control should pop the stack. At the root, disable the control rather than wrapping to an arbitrary node.
  5. Preserve focus after updates: if data refreshes, locate the focused node by stable ID. If it disappeared, move to the nearest surviving ancestor.
  6. Communicate state: update the heading, browser-accessible live region, and keyboard focus so users know which hierarchy level they are viewing.

One documented treemap application supports double-clicking a grouped node or pressing Down to drill in, and uses another drill action or Up to move out when its grouping option is active; see the TreeMap user guide. That demonstrates one interaction design, not a universal charting convention.

Keyboard and pointer details

  • Give each visible rectangle a focusable element with an accessible name containing the node label and value.
  • Use Enter or Space for the same action as the primary activation gesture.
  • Do not rely on hover to reveal the only value or navigation control; touch devices have no hover.
  • Keep a visible selected outline that is distinct from the rectangle’s color encoding.
  • When entering a group, move focus to the new group heading or first child and announce the new path.

Using D3 when you want a layout engine

Hand-building the algorithm is useful when you need custom packing, instrumentation, or unusual interaction. If you mainly need a reliable layout, D3’s d3-hierarchy module already computes node edges named x0, y0, x1, and y1. Aggregate first, then pass the hierarchy to the treemap:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const root = d3.hierarchy(data)
  .sum(d => Math.max(0, Number(d.value) || 0))
  .sort((a, b) => b.value - a.value);

const layout = d3.treemap()
  .size([width, height])
  .paddingInner(2);

layout(root);

const cells = svg.selectAll("g.cell")
  .data(root.descendants(), d => d.data.id)
  .join("g")
  .attr("class", "cell")
  .attr("transform", d => `translate(${d.x0},${d.y0})`);

cells.append("rect")
  .attr("width", d => Math.max(0, d.x1 - d.x0))
  .attr("height", d => Math.max(0, d.y1 - d.y0));

D3’s default tiling is squarified and uses a golden-ratio target as a hint. The target is not a promise that every rectangle will achieve that ratio; the available area, values, ordering, and final row constrain the result. The API also lets you replace the tiling function, which is useful when your product has a specific ordering or animation requirement. See the D3 documentation for the current API.

Drill-down still belongs in your event handlers and state management. A D3 layout will not decide whether a double-click changes focus, whether a breadcrumb appears, or how keyboard users navigate. You can use the same focus-stack design described above while asking D3 to recompute rectangles for the newly focused node.

Choosing a tiling strategy

Strategy Shape and ordering Best fit Trade-off
Squarified Greedily seeks near-square rectangles. Comparing values and selecting individual nodes. Ordering can change as values change; small items may still be thin.
Slice-and-dice Alternates horizontal and vertical slices. Very simple implementations or strict parent-child reading order. Can create long, thin rectangles that are harder to compare.
Strip Keeps items in ordered strips. Content that must follow a predictable sequence. Usually gives up some aspect-ratio quality for order.
Resquarify Preserves topology and prior adjacencies when the same target ratio is reused. Animated updates where visual continuity matters. Later layouts are not as optimal because only the first layout uses the full squarified algorithm.

D3 documents the strip, squarified, and resquarify choices and their intended trade-offs. The API reference describes resquarify as an option for stable animation: it reduces shuffling and occlusion by preserving adjacency, but sacrifices fresh packing quality on subsequent layouts. The TreeMap guide also describes strip layout as ordered and predictable.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Visual hierarchy, labels, and shaded frames

Nested rectangles can become visually ambiguous when every level uses the same border and fill. Use a restrained parent frame, a consistent padding convention, and a breadcrumb that names the current path. The original squarified-treemap paper discusses shaded frames as a way to strengthen the visible hierarchy; treat that as an optional visual treatment, not as a replacement for labels.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Label only when the rectangle has enough room. For smaller cells, provide a tooltip or an accessible name, but do not let text overflow into a sibling. A value label should state its unit and aggregation level—for example, “Engineering — $2.4M” versus “Cloud hosting — $180K”—so readers do not mistake a child value for the root total.

Performance and update strategy

The layout visits each visible node, so a full recomputation is generally linear in the number of nodes after sorting. Sorting dominates each sibling set. For large, frequently changing datasets, cache stable child order, update only the focused branch, and avoid rebuilding DOM elements whose IDs and geometry did not change.

Use resquarify only when preserving adjacency improves comprehension more than a freshly optimized pack. For live dashboards, animate rectangle transitions rather than instantly teleporting them, but cap animation duration and provide a reduced-motion path. A stable layout that is slightly less compact can be easier to follow than a perfect pack that reshuffles every update.

A practical test checklist

  • Verify that the sum of child rectangle areas matches the parent area within a small floating-point tolerance.
  • Check that no sibling rectangles overlap and that none extends outside its parent after padding.
  • Confirm that a negative or non-finite value fails clearly instead of producing NaN coordinates.
  • Test one child, two children, equal values, one dominant value, all-zero children, and a very deep hierarchy.
  • Resize the viewport repeatedly and ensure the focused node and breadcrumb remain correct.
  • Navigate entirely by keyboard, including entering a group, moving among siblings, and returning to the root.
  • Refresh data while drilled down and verify the stable-ID fallback when the focused node is removed.
  • Inspect color contrast, visible focus indicators, text clipping, and reduced-motion behavior.

What “by hand” gives you

A custom implementation gives you control over validation, ordering, padding, minimum hit areas, focus history, keyboard semantics, and how updates animate. It also makes the cost visible: you must test numerical edge cases, handle accessibility, and maintain interaction code separately from geometry.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The key design boundary is simple. Squarification decides where rectangles go; your application decides what those rectangles mean when a person selects, opens, or leaves them. Once that boundary is explicit, you can use a hand-built algorithm, D3’s layout, or another library without confusing a packing strategy with a drill-down feature.

Quick Recap

SaleBestseller No. 1
Storytelling with Data: A Data Visualization Guide for Business Professionals
Storytelling with Data: A Data Visualization Guide for Business Professionals
Wiley; Language: english; Book - storytelling with data: a data visualization guide for business professionals
$15.74

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.