Skip to content

fix(donut-s2): Direct/Advanced label collision avoidance and ring-overlap truncation - #905

Open
madelineluke wants to merge 3 commits into
feat/donut-advanced-labelsfrom
feat/donut-direct-labels-collision
Open

fix(donut-s2): Direct/Advanced label collision avoidance and ring-overlap truncation#905
madelineluke wants to merge 3 commits into
feat/donut-advanced-labelsfrom
feat/donut-direct-labels-collision

Conversation

@madelineluke

Copy link
Copy Markdown
Collaborator

Description

Fixes the donut label collision-avoidance algorithm (a parenthesization bug in the running-max cascade formula was pushing labels far below the ring), wires the same collision avoidance into AdvancedLabel (which previously had none), and adds ring-overlap protection via text truncation for both SegmentLabel and AdvancedLabel.

  • Fixed getLabelCollisionTransforms's min-gap expression missing parentheses, which broke the vertical push-down math for colliding direct labels.
  • Wired collision avoidance into AdvancedLabel (swatch + name/value/detail block), previously unguarded against overlapping neighbors.
  • Replaced the flat, equator-only-correct outerRadius * ratio truncation cap with a dynamic per-label cap (DONUT_RADIUS - collisionHalfWidth) that uses each label's own actual remaining room, freeing up unused space away from the ring's equator.
  • Added a hemisphere-conditional limit (Vega ellipsis truncation) so labels only truncate when content would genuinely overlap the ring, guarding against a floating-point boundary mismatch that could truncate content that already fit.

Related Issue

N/A - direct debugging/hardening of feat/donut-advanced-labels label positioning.

Motivation and Context

Advanced/Direct labels could overlap each other or overrun the donut ring at certain sizes and content lengths; this makes both mark types self-consistently avoid collisions and gracefully truncate instead of visually breaking.

How Has This Been Tested?

  • Full donut unit test suite (272 suites / 4240+ tests) passing, including new coverage for the truncation limit expressions and collision math.
  • Manually verified in Storybook (S2) across the Direct Label and Advanced Label responsive stories, sweeping container widths through all size tiers.

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have signed the Adobe Open Source CLA.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

madelineluke and others added 3 commits September 3, 2026 11:31
…rktree

Restores the WIP collision cascade algorithm (donutLabelCollisionUtils.ts)
from an earlier stash, resolved against changes landed since (advanced-labels).
Also fixes one stale integration test that expected the old fontSize-0
hiding mechanism for thin segments; the WIP now excludes them from the
label data source entirely instead.

Known issue still to debug: a positioning bug pushes labels too far in
Safari/Firefox (see the "slivers" story) - not yet root-caused.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause of labels being pushed far below the ring (the known
Safari/Firefox-in-slivers-story bug): getLabelCollisionTransforms
substituted minGapExpr (a bare additive expression) into
"idealY - minGapExpr * rank" without parentheses, so operator
precedence silently turned it into
"idealY - nameFontSize + valueFontSize + (16 * rank)" instead of
"idealY - (nameFontSize + valueFontSize + 16) * rank". The error
compounded across the sorted cascade, producing large spurious
pushes for later-ranked labels. Fixed by parenthesizing minGapExpr
before every multiplication.

Also wires the (now-fixed) collision algorithm into AdvancedLabel,
which previously had no collision avoidance at all - added
getAdvancedLabelData mirroring getSegmentLabelData, switched the
swatch/text marks from polar radius+theta positioning to the
collision-adjusted x/y fields, and switched the hemisphere check
from raw arcTheta to the derived hemisphere field throughout.

Also fixes a latent bug in segmentLabelUtils.ts where the baseline
signal still used the pre-collision arcTheta instead of the
collision-adjusted position, inconsistent with its own dy logic.

Note: this does not address the separate "smoosh" issue (a single
label's own row-stacking cap compressing its rows when its block is
too tall for its reserved space near the ring's pole) - that remains
a distinct, still-open problem from label-vs-label collision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n fix

Two follow-up fixes to the collision-cascade positioning work, both
found via live testing:

1. The horizontal reach cap for left-hemisphere labels was a flat
   outerRadius*ratio value, derived for the worst case of a label
   sitting exactly at the ring's equator. Away from the equator (most
   labels), the ring's real half-width is smaller, leaving genuine
   unused space between the label and the container's actual edge.
   Replaced with a per-label dynamic cap (available radius minus that
   label's own ring half-width at its actual Y), which equals the old
   flat cap exactly at the equator but grows for every label away from
   it.

2. When a row's content is narrower than the cap (the common case),
   the cap equals the row's own exact natural width - a razor's-edge
   boundary where our own getLabelWidth (canvas) measurement and
   Vega's internal text-truncation math can round against each other
   by a fraction of a pixel, truncating a character that didn't need
   to go. Verified canvas measureText and real SVG rendering agree to
   within 0.01px for the same text/font, ruling out a font mismatch -
   the fix is to only apply a real `limit` when content genuinely
   exceeds the cap, passing Vega's "no limit" value (0) otherwise.

Also adds a dedicated DONUT_ADVANCED_LABEL_RING_GAP (currently 20px,
same as direct labels - still open for design confirmation) separate
from the shared DONUT_LABEL_RING_GAP, since AdvancedLabel's bulkier
swatch+multi-row block was found to need a different value than
SegmentLabel's simpler two-line block.

Adds unit test coverage for all of the above (getDonutOuterRadiusExpr's
per-label-type ring gap selection, the dynamic cap formula, and the
truncation-limit boundary guard) and fixes the three existing unit
tests that asserted the old flat-ratio cap expression string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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