Skip to content

fix(charts-core): keep zero-valued cells on their stack baseline in the diverging offset - #70

Open
Nickman87 wants to merge 1 commit into
TanStack:mainfrom
Nickman87:fix/diverging-offset-zero-baseline
Open

fix(charts-core): keep zero-valued cells on their stack baseline in the diverging offset#70
Nickman87 wants to merge 1 commit into
TanStack:mainfrom
Nickman87:fix/diverging-offset-zero-baseline

Conversation

@Nickman87

@Nickman87 Nickman87 commented Aug 8, 2026

Copy link
Copy Markdown

Problem

A stacked areaY/areaX/barY with an exact-zero value renders a spike to the axis instead of a flat segment at the current baseline.

stackExtents defaults to d3's stackOffsetDiverging, which assigns the extent [0, 0] to any cell whose value is exactly 0. d3 does this on purpose — zero has no side, so it is stacked at zero — and for bar marks that is fine, since a zero-height band is invisible wherever it sits. Area and line marks interpolate between adjacent positions, so the same rule makes the band's edges collapse to the axis and return, cutting through every band below it.

before-buggy

Shows a stacked areaY with two series ("Allowed" climbing, "Creating" idle at 0 for one stage); the "Creating" band spikes to the axis and cuts through "Allowed" underneath.

The bug is not confined to the topmost series. It affects any series that is not the first on its side of the axis, in both directions:

stack (order A → B) position with the zero current expected
A=10, B=5 / A=10, B=0 B [0, 0] [10, 10]
A=-10, B=-5 / A=-10, B=0 B [0, 0] [-10, -10]
A=10, B=-5, C=3 / …, C=0 C [0, 0] [10, 10]

Every stack in the library is affected, because diverging is the default offset and stack-internal.ts backs area, area-x, bar and transform-stack alike. Applications currently have to pre-perturb their data with an epsilon to avoid it.

Fix

Replace the stackOffsetDiverging import with a local offset that keeps d3's positive/negative split but resolves a zero-valued cell to the baseline of whichever side its own series occupies, rather than to the axis. A series is treated as negative-side only when it is exclusively negative; anything else (positive, mixed, all-zero) resolves to the positive baseline, which matches Observable Plot's stack transform (else if (y >= 0) yp = Y2[i] = (Y1[i] = yp) + y).

stackOffsetExpand, stackOffsetSilhouette and stackOffsetWiggle all delegate to stackOffsetNone, which accumulates zeros correctly, so they need no change.

after-fixed

Same dataset as above, after the fix: the "Creating" band pinches flat against "Allowed" at the boundary instead of diving to the axis.

Behaviour change

Only cells whose value is exactly 0 move, and only when the running baseline on their side is already nonzero. Verified unchanged:

  • a zero in the first series on its side (baseline is already 0)
  • a position where every series is zero
  • mixed diverging stacks where the zero series is the only negative one
  • non-finite values (NaN extents preserved, so gaps still render as gaps)

No public API, type, or option changes. StackOffset keeps its four documented values.

Tests

Added to packages/charts-core/src/stack-internal.test.ts:

  • zero in a positive stack keeps the running top ([10, 10], not [0, 0])
  • zero in an exclusively-negative stack keeps the running bottom ([-10, -10])
  • zero in a mixed diverging stack is unchanged
  • an all-zero position is unchanged
  • stackRowsX / stackRowsY agree with stackExtents on the above

Changeset

patch for @tanstack/charts (fixed release group covers all adapters).

Alternative considered

Selecting stackOffsetNone whenever the data contains no negative values is a two-line change and fixes the reported case, but leaves the negative-side and mixed-stack variants of the same bug in place. Not worth splitting into two fixes.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed stacked charts so zero-valued cells remain on the correct running baseline when using diverging stacking.
    • Prevented stacked-area segments from incorrectly spiking to the axis.
    • Preserved consistent geometry for both vertical and horizontal stacked charts, including mixed positive and negative values.

…he diverging offset

d3's stackOffsetDiverging assigns [0, 0] to a cell whose value is exactly
zero, parking it at the axis instead of its stack's running baseline. That
is invisible for bars but not for area/line marks, whose paths interpolate
between adjacent positions, producing a spike to the axis and back that
cuts through the layers below.

Replace stackOffsetDiverging with a local zero-aware variant that keeps a
zero-valued cell on the baseline of whichever side its own series occupies.
A series is treated as negative-side only when it is exclusively negative;
everything else resolves to the positive baseline.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8dbc8104-ee9d-40f0-8164-877ad57c9e02

📥 Commits

Reviewing files that changed from the base of the PR and between b869067 and 686f0a9.

📒 Files selected for processing (3)
  • .changeset/mellow-hamster-zero-stack.md
  • packages/charts-core/src/stack-internal.test.ts
  • packages/charts-core/src/stack-internal.ts

📝 Walkthrough

Walkthrough

The default diverging stack offset now preserves zero-valued cells on their series’ running baseline. Tests cover positive, negative, mixed, and all-zero stacks, plus vertical and horizontal materialized rows. A patch changeset documents the correction.

Changes

Zero-aware diverging stacking

Layer / File(s) Summary
Implement zero-aware diverging offset
packages/charts-core/src/stack-internal.ts
The default diverging offset uses stackOffsetDivergingZeroAware. The implementation preserves positive and negative baselines for zero-valued cells and retains d3-compatible handling of non-finite values.
Validate zero-valued stack geometry
packages/charts-core/src/stack-internal.test.ts, .changeset/mellow-hamster-zero-stack.md
Tests cover positive-only, negative-only, mixed, and all-zero stacks. Integration tests verify consistent vertical and horizontal row geometry. The changeset documents the patch release.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: tannerlinsley

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the zero-baseline fix for diverging stacking in charts-core.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant