Skip to content

fix(xychart): prevent flipped point label from clipping x-axis#7552

Open
DominicBurkart wants to merge 19 commits into
mermaid-js:developfrom
DominicBurkart:fix/7549_xychart-label-axis-clipping
Open

fix(xychart): prevent flipped point label from clipping x-axis#7552
DominicBurkart wants to merge 19 commits into
mermaid-js:developfrom
DominicBurkart:fix/7549_xychart-label-axis-clipping

Conversation

@DominicBurkart
Copy link
Copy Markdown

@DominicBurkart DominicBurkart commented Mar 29, 2026

📑 Summary

When a label is auto-flipped to avoid line collision, it can clip into the axis area if the data point is near a plot boundary. This adds full bounds checking to both placement functions, covering all four plot boundaries for both chart orientations.

Stacked on #7550 and #7551. Please merge those first.

[existing] top placement (default):

Screenshot From 2026-04-05 11-25-56

[existing] bottom placement (avoid intersecting with the line graph at acute angles):

Screenshot From 2026-04-05 11-26-04

[new] forces intersection with line instead of intersecting with the x axis:
Screenshot From 2026-04-05 11-26-15

📏 Design Decisions

  • Exposes getRange() on the Axis interface — was already implemented on BaseAxis but not part of the public interface. Provides the inner plot boundaries needed for bounds checks.
  • Bounds checking integrated into placement functionscomputeLabelPlacementVertical and computeLabelPlacementHorizontal now accept a PlotBounds parameter. A candidate position is rejected if it clips any axis boundary, not just the x-axis bottom.
  • Fallback: non-flipped at base offset — if all 6 candidates (3 offsets × 2 directions) either collide with the line or clip a boundary, the label stays above/right at minimum offset. This prefers line overlap over axis clipping.

Coverage

Orientation Boundary Handled
Vertical x-axis (bottom)
Vertical top of plot
Horizontal y-axis (right)
Horizontal left boundary

Changes

File Change
axis/index.ts Add getRange() to Axis interface
linePlot.ts Add PlotBounds interface; integrate bounds checks into computeLabelPlacementVertical and computeLabelPlacementHorizontal
demos/xychart.html Add axis-bounds demo charts for both orientations
.gitignore Ignore demos/mermaid.esm.mjs build artifact

📋 Tasks

  • 📖 have read the contribution guidelines
  • 💻 have added necessary unit/e2e tests.
  • 📓 have added documentation. Make sure MERMAID_RELEASE_VERSION is used for all new features.
  • 🦋 If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

claude and others added 5 commits March 29, 2026 12:59
Allow annotating individual data points on xychart line plots with
custom text labels. The syntax extends the existing data array format:

  line [540 "PaLM", 65 "LLaMA", 34 "Llama 2", 7 "Mistral"]

Each number can optionally be followed by a quoted string label.
Labels are rendered above (vertical) or to the right (horizontal)
of the data point, using the line's stroke color.

Backward-compatible: existing syntax without labels works unchanged.

Addresses mermaid-js#5326.

https://claude.ai/code/session_01CgvREhkQ3tVYCagq1mHmjV
Document the new per-point text label syntax for line charts,
including syntax examples and rendering behavior notes.

https://claude.ai/code/session_01WErEVQAYaNmC3rpbevFwMg
…labels

- Add minor changeset for the new point labels feature
- Enhance xyChart.md docs with mermaid-example block for mixed labels
  and a note about fixed font size
- Add 4 Cypress e2e snapshot tests covering: all-labeled, mixed-labels,
  horizontal orientation, and multi-line scenarios
- Add MMLU to cspell dictionary

https://claude.ai/code/session_011g3A6y2kdJnZh8YB4uJaxD
When a line segment has a steep slope, labels placed above a data point
can collide with the line. This adds collision detection that estimates
each label's bounding box and checks for intersection with adjacent line
segments using y-range overlap. When a collision is detected, the label
is flipped to the opposite side of the point (below for vertical charts,
left for horizontal charts).

https://claude.ai/code/session_01UCV1QCaRsRtQjRspaVrqUp
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 29, 2026

🦋 Changeset detected

Latest commit: 5b183bf

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
mermaid Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 29, 2026

Deploy Preview for mermaid-js ready!

Name Link
🔨 Latest commit 5b183bf
🔍 Latest deploy log https://app.netlify.com/projects/mermaid-js/deploys/69eca4ea72ad2b00086f97ff
😎 Deploy Preview https://deploy-preview-7552--mermaid-js.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 29, 2026

Open in StackBlitz

@mermaid-js/examples

npm i https://pkg.pr.new/@mermaid-js/examples@7552

mermaid

npm i https://pkg.pr.new/mermaid@7552

@mermaid-js/layout-elk

npm i https://pkg.pr.new/@mermaid-js/layout-elk@7552

@mermaid-js/layout-tidy-tree

npm i https://pkg.pr.new/@mermaid-js/layout-tidy-tree@7552

@mermaid-js/mermaid-zenuml

npm i https://pkg.pr.new/@mermaid-js/mermaid-zenuml@7552

@mermaid-js/parser

npm i https://pkg.pr.new/@mermaid-js/parser@7552

@mermaid-js/tiny

npm i https://pkg.pr.new/@mermaid-js/tiny@7552

commit: 0ea8d7b

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 29, 2026

Codecov Report

❌ Patch coverage is 0% with 232 lines in your changes missing coverage. Please review.
✅ Project coverage is 3.32%. Comparing base (e9d4c11) to head (0ea8d7b).
⚠️ Report is 254 commits behind head on develop.

Files with missing lines Patch % Lines
...s/xychart/chartBuilder/components/plot/linePlot.ts 0.00% 212 Missing ⚠️
...rams/xychart/chartBuilder/components/plot/index.ts 0.00% 10 Missing ⚠️
packages/mermaid/src/diagrams/xychart/xychartDb.ts 0.00% 9 Missing ⚠️
.../src/diagrams/xychart/chartBuilder/orchestrator.ts 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           develop   #7552      +/-   ##
==========================================
- Coverage     3.34%   3.32%   -0.03%     
==========================================
  Files          524     535      +11     
  Lines        55256   56456    +1200     
  Branches       795     820      +25     
==========================================
+ Hits          1850    1876      +26     
- Misses       53406   54580    +1174     
Flag Coverage Δ
unit 3.32% <0.00%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...rams/xychart/chartBuilder/components/axis/index.ts 3.84% <ø> (ø)
...id/src/diagrams/xychart/chartBuilder/interfaces.ts 10.00% <ø> (ø)
.../src/diagrams/xychart/chartBuilder/orchestrator.ts 0.62% <0.00%> (ø)
packages/mermaid/src/diagrams/xychart/xychartDb.ts 0.00% <0.00%> (ø)
...rams/xychart/chartBuilder/components/plot/index.ts 1.35% <0.00%> (-0.10%) ⬇️
...s/xychart/chartBuilder/components/plot/linePlot.ts 0.00% <0.00%> (ø)

... and 36 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…padding

- Replace shouldFlipLabelVertical/Horizontal with computeLabelPlacement
  functions that verify both above and below positions before deciding
  where to place each point label
- Try increasing offsets (1x, 2x, 3x baseOffset) if both positions
  collide at the default offset — handles zigzag patterns
- Expand collision bounding boxes by strokeWidth/2 to account for the
  visual width of the rendered line
- Increase default labelOffset from 10 to 14 for more breathing room
- Add reviewer's exact failing example to e2e tests and demo page
When shouldFlipLabelVertical() recommends placing a label below its
data point (to avoid line collision), check that the flipped position
stays within yAxis.getRange() — the inner boundary of the plot area.
If the flipped position would collide with the x-axis, keep the label
above instead.

Exposes getRange() on the Axis interface (already implemented on
BaseAxis) so linePlot.ts can query the plot boundary.
@DominicBurkart DominicBurkart force-pushed the fix/7549_xychart-label-axis-clipping branch from 9cbc71e to fc44f99 Compare April 4, 2026 21:15
@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented Apr 4, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Awaiting the start of a new Argos build…

Copy link
Copy Markdown
Collaborator

@knsv-bot knsv-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[sisyphus-bot]

Hey @DominicBurkart — this is a really well-crafted feature contribution! The stacked PR structure is appreciated, the collision avoidance algorithm is solid, and the test coverage is thorough. Let's work through a couple of items to get this across the finish line.

Note: This review covers the full stacked diff (#7550 + #7551 + #7552). Since #7550 and #7551 need to merge first, the items below apply across the stack.


File Triage

Tier Count Files
Tier 2 (diff + context) 7 axis/index.ts, linePlot.ts, interfaces.ts, xychart.jison, xychart.jison.spec.ts, xychartDb.ts, xyChart.spec.js
Tier 3 (diff only) 5 3 changesets, 2 docs files
Tier 3 (minor) 3 .cspell, .gitignore, demos/xychart.html

What's working well

🎉 [praise] The collision avoidance algorithm in linePlot.ts is excellent. The doesSegmentIntersectBox function correctly handles the tricky case where a steep line enters from above and exits below a bounding box without any endpoint or sample falling inside. The bounded retry loop (3 offsets × 2 directions, then fallback) is a clean design that won't degrade on edge cases.

🎉 [praise] Test coverage is comprehensive — 6 new parser unit tests covering labels with spaces, mixed labeled/unlabeled, decimals, and bar data passthrough, plus 8 E2E visual regression tests covering vertical, horizontal, mixed collision, and steep descent scenarios. This is exactly the level of coverage the project needs.

🎉 [praise] Clean interface extension — getRange() was already implemented on BaseAxis and used internally by both BandAxis and LinearAxis. Promoting it to the Axis interface is the right call and adds no risk.


Things to address

🟡 [important] Hardcoded font size and label offset

linePlot.ts:269-270fontSize = 12 and labelOffset = 14 are hardcoded constants. The existing xychart config already has labelFontSize on the axis configs, and the renderer already handles configurable font sizes for other text elements.

It would be great to derive label font size from config (or at least from theme variables) rather than hardcoding — this ensures labels scale consistently with the rest of the chart when users customize their config. If there's no suitable config key today, adding one to XYChartConfig (with 12px as default) would be a small addition.

This doesn't need to block the initial merge, but it would be good to address before the feature ships to avoid a follow-up breaking change to label sizing.

🟡 [important] Character width estimation may cause false collision/non-collision

linePlot.ts:4CHAR_WIDTH_FACTOR = 0.7 estimates text width as fontSize * labelText.length * 0.7. This works reasonably for monospace but not for proportional fonts (which mermaid uses by default). A label like "WWWW" will be significantly wider than estimated, potentially colliding with the line despite the algorithm saying it's safe. Conversely "iiii" will be narrower, causing unnecessary flips.

The codebase already has TextDimensionCalculatorWithFont (used in textDimensionCalculator.ts, imported by the axis components) which measures actual rendered text dimensions via a temporary SVG element. Using this for label width measurement would give accurate bounding boxes for collision detection. It would require passing the calculator into LinePlot or computing dimensions in the getDrawableElement method.

🟢 [nit] Generated docs file committed

docs/syntax/xyChart.md — This is auto-generated from packages/mermaid/src/docs/syntax/xyChart.md (which is correctly updated). CI/autofix handles regeneration, so this file doesn't need to be in the PR diff. Not a problem — CI will sort it out — but it can be dropped from the commit to keep the diff focused.

🟢 [nit] Bar labels silently dropped

The grammar change to dataPoints applies to both line and bar plot data, so bar [10 "A", 20 "B"] is valid syntax. But setBarData in xychartDb.ts only extracts value and ignores labels. This is fine behavior, but it would be worth noting in the docs that labels are currently line-only, so users don't wonder why their bar labels don't render.

💡 [suggestion] Consider reusing existing text measurement infrastructure

If adopting TextDimensionCalculatorWithFont feels too heavy for this feature, an intermediate approach would be to use a slightly more conservative CHAR_WIDTH_FACTOR (e.g., 0.85) to reduce false negatives with wide characters, while documenting the known limitation. The current 0.7 factor may underestimate by 20-30% for worst-case proportional text.


Security

No XSS or injection issues identified. Label text flows from the JISON parser (STR token) → xychartDb.tsLinePlotData.pointLabelsTextElem.text → rendered via D3's .text() method (xychartRenderer.ts:242), which safely sets textContent (not innerHTML). The final SVG output is still sanitized by DOMPurify in the standard pipeline.


Self-check

  • At least one 🎉 [praise] item exists (3)
  • No duplicate comments
  • Severity tally: 0 🔴 blocking / 2 🟡 important / 2 🟢 nit / 1 💡 suggestion / 3 🎉 praise
  • Verdict matches criteria: COMMENT (2 🟡, no 🔴)
  • Not a draft — COMMENT is appropriate given no blocking issues
  • No inline comments used
  • Tone check: collaborative and appreciative ✓

claude and others added 8 commits April 12, 2026 18:21
Allow annotating individual data points on xychart line plots with
custom text labels. The syntax extends the existing data array format:

  line [540 "PaLM", 65 "LLaMA", 34 "Llama 2", 7 "Mistral"]

Each number can optionally be followed by a quoted string label.
Labels are rendered above (vertical) or to the right (horizontal)
of the data point, using the line's stroke color.

Backward-compatible: existing syntax without labels works unchanged.

Addresses mermaid-js#5326.

https://claude.ai/code/session_01CgvREhkQ3tVYCagq1mHmjV
Document the new per-point text label syntax for line charts,
including syntax examples and rendering behavior notes.

https://claude.ai/code/session_01WErEVQAYaNmC3rpbevFwMg
…labels

- Add minor changeset for the new point labels feature
- Enhance xyChart.md docs with mermaid-example block for mixed labels
  and a note about fixed font size
- Add 4 Cypress e2e snapshot tests covering: all-labeled, mixed-labels,
  horizontal orientation, and multi-line scenarios
- Add MMLU to cspell dictionary

https://claude.ai/code/session_011g3A6y2kdJnZh8YB4uJaxD
- Pass point labels through textSanitizer() in setLineData, matching
  the defense-in-depth pattern already used for axis titles/bands
- Add note to docs that labels apply to line plots only; bar syntax
  is accepted but labels are currently ignored
When a line segment has a steep slope, labels placed above a data point
can collide with the line. This adds collision detection that estimates
each label's bounding box and checks for intersection with adjacent line
segments using y-range overlap. When a collision is detected, the label
is flipped to the opposite side of the point (below for vertical charts,
left for horizontal charts).

https://claude.ai/code/session_01UCV1QCaRsRtQjRspaVrqUp
…padding

- Replace shouldFlipLabelVertical/Horizontal with computeLabelPlacement
  functions that verify both above and below positions before deciding
  where to place each point label
- Try increasing offsets (1x, 2x, 3x baseOffset) if both positions
  collide at the default offset — handles zigzag patterns
- Expand collision bounding boxes by strokeWidth/2 to account for the
  visual width of the rendered line
- Increase default labelOffset from 10 to 14 for more breathing room
- Add reviewer's exact failing example to e2e tests and demo page
@DominicBurkart DominicBurkart force-pushed the fix/7549_xychart-label-axis-clipping branch from 8b03b9b to 0eb5849 Compare April 12, 2026 16:38
@DominicBurkart DominicBurkart force-pushed the fix/7549_xychart-label-axis-clipping branch from 63d19c0 to 4f7a521 Compare April 12, 2026 17:37
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 12, 2026

Lockfile Validation Failed

The following issue(s) were detected:

Please address these and push an update.

Posted automatically by GitHub Actions

When shouldFlipLabelVertical() recommends placing a label below its
data point (to avoid line collision), check that the flipped position
stays within yAxis.getRange() — the inner boundary of the plot area.
If the flipped position would collide with the x-axis, keep the label
above instead.

Exposes getRange() on the Axis interface (already implemented on
BaseAxis) so linePlot.ts can query the plot boundary.
@DominicBurkart DominicBurkart force-pushed the fix/7549_xychart-label-axis-clipping branch 3 times, most recently from 7718e0e to 0f27fec Compare April 12, 2026 18:24
…collision bounds

Replace the CHAR_WIDTH_FACTOR = 0.7 width approximation with measured
text dimensions via TextDimensionCalculatorWithFont. This matches what
the renderer actually draws, so collision detection correctly flips
labels whose real rendered width would overlap an adjacent line segment.

BasePlot now owns a TextDimensionCalculatorWithFont instance (constructed
from tmpSVGGroup) and passes it into LinePlot, which calls getMaxDimension
per label. Placement helpers take a Dimension instead of reconstructing
width from a char-count heuristic.
@DominicBurkart DominicBurkart force-pushed the fix/7549_xychart-label-axis-clipping branch from 8f216b2 to 0ea8d7b Compare April 12, 2026 18:40
…ping' into fix/7549_xychart-label-axis-clipping

# Conflicts:
#	docs/syntax/xyChart.md
#	packages/mermaid/src/diagrams/xychart/chartBuilder/components/plot/linePlot.ts
#	packages/mermaid/src/diagrams/xychart/xychartDb.ts
#	packages/mermaid/src/docs/syntax/xyChart.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Bug / Error Something isn't working or is incorrect

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants