From c38dec9f1edca171810a589a6c2d733bb3dfd8ff Mon Sep 17 00:00:00 2001 From: waterWang Date: Sun, 9 Aug 2026 07:30:17 +0800 Subject: [PATCH] fix: correct left-edge guard for dataLabels textAnchor:start in horizontal bar charts (Closes #5094) --- src/charts/common/bar/DataLabels.js | 13 +-- ...ssue-5094-datalabel-horizontal-bar.spec.js | 88 +++++++++++++++++++ 2 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 tests/unit/issue-5094-datalabel-horizontal-bar.spec.js diff --git a/src/charts/common/bar/DataLabels.js b/src/charts/common/bar/DataLabels.js index c1c8a7f92..5a517ef22 100644 --- a/src/charts/common/bar/DataLabels.js +++ b/src/charts/common/bar/DataLabels.js @@ -560,12 +560,13 @@ export default class BarDataLabels { if (!w.config.chart.stacked) { if (dataLabelsConfig.textAnchor === 'start') { - if (dataLabelsX - textRects.width < 0) { - // Pin the label to the plot edge, but keep honoring the user's - // offsetX from that edge — otherwise a `position:'bottom'` + - // `textAnchor:'start'` label (whose dataLabelsX equals the offset for - // left-rooted bars) silently loses any offsetX smaller than its own - // text width. offX is 0 by default, so this is a no-op unless set. + if (dataLabelsX < 0) { + // Pin the label to the plot edge when the anchor point itself + // is off-screen to the left. textAnchor:'start' means the text + // extends to the RIGHT from dataLabelsX, so only the anchor + // position matters — not the text width. Previously used + // `dataLabelsX - textRects.width < 0` which incorrectly pinned + // labels whose text was wider than the bar (issue #5094). dataLabelsX = valIsNegative ? textRects.width + strokeWidth - offX : strokeWidth + offX diff --git a/tests/unit/issue-5094-datalabel-horizontal-bar.spec.js b/tests/unit/issue-5094-datalabel-horizontal-bar.spec.js new file mode 100644 index 000000000..05784b4f9 --- /dev/null +++ b/tests/unit/issue-5094-datalabel-horizontal-bar.spec.js @@ -0,0 +1,88 @@ +import { createChartWithOptions } from './utils/utils.js' + +// Integration test for #5094 — Datalabel misaligned when text is wider than bar +// in horizontal bar chart with textAnchor:'start'. +// +// In jsdom, getTextRects returns a fixed width of ~10px, so the original bug +// condition (dataLabelsX - textRects.width < 0) cannot be triggered. However, +// the fix is semantically correct: for textAnchor 'start' the text extends +// RIGHT from the anchor, so the left-edge overflow condition must check +// `dataLabelsX < 0` (the anchor itself), not `dataLabelsX - textRects.width < 0` +// (which is designed for textAnchor 'end'). +// +// This test verifies that: +// 1. All data labels are rendered (4 bars → 4 labels) +// 2. All x positions are positive and inside the plot +// 3. x positions are monotonic with their values (proportional) + +describe('datalabel position in horizontal bar (issue #5094)', () => { + function horizontalBarChart(overrides = {}) { + return createChartWithOptions({ + chart: { + type: 'bar', + width: '800px', + height: 300, + parentHeightOffset: 0, + toolbar: { show: false }, + animations: { enabled: false }, + }, + series: [{ name: 'categories', data: [17332, 2235, 1251, 1093] }], + plotOptions: { + bar: { + horizontal: true, + distributed: true, + barHeight: '80%', + dataLabels: { position: 'top', maxItems: 100 }, + }, + }, + xaxis: { labels: { show: false }, axisBorder: { show: true }, axisTicks: { show: false } }, + yaxis: [{ labels: { minWidth: 80 } }], + dataLabels: { enabled: true, textAnchor: 'start', offsetX: 10 }, + legend: { show: false }, + grid: { show: false }, + tooltip: { enabled: false }, + ...overrides, + }) + } + + it('renders all 4 data labels', () => { + const chart = horizontalBarChart() + const labels = chart.el.querySelectorAll('.apexcharts-datalabel') + expect(labels.length).toBe(4) + }) + + it('places all labels with positive x positions inside the plot', () => { + const chart = horizontalBarChart() + const xs = Array.from(chart.el.querySelectorAll('.apexcharts-datalabel')) + .map((l) => parseFloat(l.getAttribute('x'))) + xs.forEach((x) => expect(x).toBeGreaterThanOrEqual(0)) + }) + + it('orders labels proportionally to their values (bigger value → bigger x)', () => { + const chart = horizontalBarChart() + const labels = chart.el.querySelectorAll('.apexcharts-datalabel') + const data = Array.from(labels).map((l) => ({ + x: parseFloat(l.getAttribute('x')), + text: l.textContent, + })) + // Sort by value descending (since horizontal bar with position 'top' + // places the biggest bar's label at the rightmost x) + const sorted = data.sort((a, b) => parseFloat(b.text) - parseFloat(a.text)) + // Verify x positions decrease as values decrease + for (let i = 1; i < sorted.length; i++) { + expect(sorted[i].x).toBeLessThan(sorted[i - 1].x) + } + }) + + it('does not pin labels to the left edge', () => { + // Even the smallest bar's label should be at a position proportional + // to its value, not pinned to strokeWidth + offsetX (~11px). + const chart = horizontalBarChart() + const xs = Array.from(chart.el.querySelectorAll('.apexcharts-datalabel')) + .map((l) => parseFloat(l.getAttribute('x'))) + const minX = Math.min(...xs) + // The smallest bar (1093) in a 683px grid with max scaling should + // have its label at least ~20px from the left edge + expect(minX).toBeGreaterThan(20) + }) +}) \ No newline at end of file