Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/treemap-layout-bounds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/charts': patch
---

Handle floating-point rounding at treemap plot boundaries and omit empty tiles collapsed outside the plot by inner padding.
2 changes: 1 addition & 1 deletion benchmarks/comparison/bundle-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"tanstack": {
"kind": "workspace",
"revision": "69d70960dd6b29f3706a173d348f2fc8271b0dcc",
"inputDigest": "sha256:80f5c5447f39757715be5def33eed28b12869946936369671243436701fb7964"
"inputDigest": "sha256:8115665ae3c573649f47d236836e28c8cf14d513be1117a8e5c720e3a618680c"
},
"chartjs": {
"kind": "package",
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/conformance/previews/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"width": 288,
"height": 192,
"sourceHash": "4c9a63949cbf9511cf8abd901c2bb8224ea959d284456a9b90e1b87745400ef9",
"sourceHash": "e9da35fe64694acbe80a296e2b848ce6904a0778cb545edb87a5e94cff8b9396",
"assets": [
{
"id": "01-line-gaps",
Expand Down
57 changes: 57 additions & 0 deletions packages/charts-core/src/hierarchy-treemap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,63 @@ function allNodes(nodes: readonly SceneNode[]): SceneNode[] {
}

describe('treemap', () => {
it.each(['non-finite', 'out-of-bounds'])(
'still rejects %s custom layouts',
(kind) => {
const method: TreemapTile<PathRow> = (node, x0, y0, x1, y1) => {
treemapDice(node, x0, y0, x1, y1)
const child = node.children?.[0]
if (!child)
throw new Error('Expected a child in the custom tile fixture')
if (kind === 'non-finite') child.x1 = Infinity
else child.x1 = x1 + 1
}
expect(() =>
scene(
treemap(rows, {
path: 'path',
delimiter: '.',
value: 'value',
method,
}),
),
).toThrow(`layout produced ${kind} coordinates`)
},
)

it.each([
[361, [50, 30, 20, 10, 5, 1], 0, 6],
[593, [7, 5, 3, 2, 1], 0, 5],
[804, [90, 7, 5, 4, 2, 1], 0, 6],
[600, [1000, 1], 3, 1],
])(
'keeps rounded and collapsed tiles in bounds at width %s',
(width, values, paddingInner, count) => {
const result = scene(
treemap(
values.map((value, i) => ({
path: `root.${i}`,
value,
family: 'root',
})),
{ path: 'path', delimiter: '.', value: 'value', paddingInner },
),
width,
300,
)
const rects = allNodes(result.nodes).filter(
(node) => node.kind === 'rect',
)
expect(rects).toHaveLength(count)
for (const rect of rects) {
expect(rect.x).toBeGreaterThanOrEqual(0)
expect(rect.y).toBeGreaterThanOrEqual(0)
expect(rect.x + rect.width).toBeLessThanOrEqual(width)
expect(rect.y + rect.height).toBeLessThanOrEqual(300)
}
},
)

it('renders path leaves in final chart bounds with exact metadata and lineage', () => {
const source = Object.freeze(rows.map((row) => Object.freeze({ ...row })))
const before = JSON.stringify(source)
Expand Down
14 changes: 13 additions & 1 deletion packages/charts-core/src/hierarchy-treemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ function assertLayoutCoordinates<TDatum>(
width: number,
height: number,
): void {
const tolerance = Math.max(width, height) * Number.EPSILON * 32
nodes.forEach((node) => {
const coordinates = [node.x0, node.y0, node.x1, node.y1]
if (!coordinates.every(Number.isFinite)) {
Expand All @@ -460,11 +461,22 @@ function assertLayoutCoordinates<TDatum>(
`treemap: layout produced reversed coordinates for node "${node.data.id}"`,
)
}
if (node.x0 < 0 || node.y0 < 0 || node.x1 > width || node.y1 > height) {
// D3 can collapse a padded tile outside the plot. Empty cells are omitted.
if (node.x0 === node.x1 || node.y0 === node.y1) return
if (
node.x0 < -tolerance ||
node.y0 < -tolerance ||
node.x1 > width + tolerance ||
node.y1 > height + tolerance
) {
throw new TypeError(
`treemap: layout produced out-of-bounds coordinates for node "${node.data.id}"`,
)
}
node.x0 = Math.max(0, node.x0)
node.y0 = Math.max(0, node.y0)
node.x1 = Math.min(width, node.x1)
node.y1 = Math.min(height, node.y1)
})
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/measure-bundles.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ const entries = [
'Hierarchy treemap mark',
'benchmarks/entries/charts-hierarchy-treemap.ts',
'D3 hierarchy treemap kernel',
3.86,
4022 / 1024,
{
inputBoundary: {
require: ['hierarchyFlat', 'hierarchyTreemap', 'd3Hierarchy'],
Expand Down