Skip to content

treemap: bounds check throws on floating-point overshoot from d3 squarify (and on paddingInner collapse) #243

Closed

Description

@quinnj

Describe the bug

treemap() throws TypeError: treemap: layout produced out-of-bounds coordinates for node "…" for ordinary data at some chart sizes, so the whole chart fails to render.

assertLayoutCoordinates in packages/charts-core/src/hierarchy-treemap.ts compares each leaf's x1/y1 with the plot width and height exactly. d3's squarify adds tile sizes in floating point, so the last tile in a row can end about 1e-13 px past the edge. That trips the check.

Reproduction

The same layout that treemap() runs, with d3-hierarchy only:

import { hierarchy, treemap, treemapSquarify } from 'd3-hierarchy';

const width = 361;
const height = 300;
const root = hierarchy({
  children: [50, 30, 20, 10, 5, 1].map((value) => ({ value })),
}).sum((d) => d.value ?? 0);

treemap().size([width, height]).tile(treemapSquarify)(root);

const leaf = root.leaves().find((n) => n.x1 > width);
console.log(leaf.x1 - width); // 5.684341886080802e-14

A TanStack treemap with these values and a 361 × 300 plot area gets the same coordinates and throws. We hit it in a real app at normal container widths. A sweep over widths 300–900 px finds a failing width for each data set we tried, for example [7, 5, 3, 2, 1] at 593 × 300 and [90, 7, 5, 4, 2, 1] at 804 × 300.

A second case: paddingInner

With paddingInner, d3 collapses a tile that is thinner than the padding to its midpoint, and that point can be outside the plot area. For example, values [1000, 1], 600 × 300, paddingInner: 3: the small tile gets x0 = x1 = 601.2, so the check throws. Zero-area cells are already filtered out right after the check, so these tiles would otherwise be dropped harmlessly.

Expected behavior

A floating-point overshoot does not throw. For example, clamp each leaf to the plot area, or compare with a small tolerance. A tile that collapsed outside the plot area is dropped, not reported as an error.

Workaround

A custom method that runs squarify and then clamps each child to its parent's box:

import { treemapSquarify } from 'd3-hierarchy';

const squarifyInBounds = (node, x0, y0, x1, y1) => {
  treemapSquarify(node, x0, y0, x1, y1);
  for (const child of node.children ?? []) {
    child.x0 = Math.max(child.x0, x0);
    child.y0 = Math.max(child.y0, y0);
    child.x1 = Math.min(child.x1, x1);
    child.y1 = Math.min(child.y1, y1);
  }
};

treemap(data, { method: squarifyInBounds /* … */ });

For gaps between tiles, inset avoids the paddingInner case, because it is applied when drawing, not in the layout.

Versions

  • @tanstack/charts 1.1.0. The same check is on main (at b6f19c2).
  • d3-hierarchy 3.1.2

Activity

  1. tannerlinsley commented on Oct 23, 2026

    @tannerlinsley
    Member

    Thanks @quinnj, the reproduction cases made both causes clear. The fix is merged in #244 and queued for release, but it is not on npm yet.

    AI review notes:
    Regression tests cover all four reported cases, including the padded tile collapse, and still reject non-finite or materially out-of-bounds custom layouts. All 15 focused tests, full local validation, and the applicable remote CI checks passed on the merged candidate.
    The change tolerates machine-scale rounding, clamps those accepted bounds, and lets the existing filter omit empty cells. The Changeset will be included in the pending release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions