Skip to content

fix: align x-axis ticks with bar positions for numeric bar charts with >10 bars (#5086) - #5259

Open
waterWang wants to merge 1 commit into
apexcharts:mainfrom
waterWang:fix/5086-xaxis-ticks-misaligned
Open

fix: align x-axis ticks with bar positions for numeric bar charts with >10 bars (#5086)#5259
waterWang wants to merge 1 commit into
apexcharts:mainfrom
waterWang:fix/5086-xaxis-ticks-misaligned

Conversation

@waterWang

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Refactor
  • Other (please describe)

What is the current behavior?

For bar charts with numeric x-axis data (e.g. {x, y} format), the x-axis tick amount is computed from Math.round(svgWidth / 150). With 11+ bars this can produce a non-integer step (e.g. 11 bars over a 600px chart → ticks=4step=2.5 → ticks [1, 3.5, 6, 8.5, 11]), so the tick labels land at non-integer positions that don't align with the bars.

Closes #5086

What is the new behavior?

For vertical bar charts with numeric x-axis data and fewer than 30 data points, the tick amount is set to dataPoints - 1, so the step is exactly 1 and every tick lands on an integer position aligned with a bar. This matches matplotlib's approach of always generating ticks at a regular interval that falls on actual data points.

Before: 11 bars on a 600px chart → ticks [1, 3.5, 6, 8.5, 11] (misaligned)
After: 11 bars on a 600px chart → ticks [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] (aligned)

What does this PR do?

  • In src/modules/Range.js, when the chart is a vertical bar chart with numeric x-axis data (isXNumeric) and dataPoints < 30, sets ticks = dataPoints - 1 so the scale uses integer steps.
  • Adds unit tests covering 11-bar and 25-bar numeric bar charts, plus a check that non-bar chart types are unaffected.

Screenshots (if appropriate)

N/A — verified via unit tests that xAxisScale.result produces integer-aligned ticks.

Other information

All 2140 unit tests pass (94 test files).

…h >10 bars (apexcharts#5086)

For bar charts with numeric x-axis data (e.g. {x, y} format), the tick
amount was computed from svgWidth/150, which can produce non-integer
steps (e.g. 11 bars → step 1.111). This caused tick labels to be
positioned at non-integer values that don't align with the bars.

Fix: when the chart is a vertical bar chart with numeric x-axis data
and fewer than 30 data points, use dataPoints - 1 as the tick amount.
This ensures step=1 so labels land on integer positions aligned with
bars, matching matplotlib's behavior of always generating ticks at
regular intervals that fall on actual data points.

@junedchhipa junedchhipa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for digging into this, and for the clear write-up and repro in the description.

The core fix is real. I verified it by rendering both revisions side by side: the 11-bar case goes from ticks [1, 3.5, 6, 8.5, 11] (labels 1, 4, 6, 9, 11) to [1..11], and the label positions now line up with the bar centres exactly (label at x=48.418 vs bar centre at 31.47 + 33.89/2 = 48.42). Your "2140 unit tests pass" claim checks out too, and the two positive tests do fail when I revert the hunk, so they are guarding real behaviour.

My concern is that ticks = gl.dataPoints - 1 is applied for every numeric bar chart under 30 points with no reference to how much room there is to draw labels. Two regressions fall out of that.

1. Narrow charts get an unreadable axis

A 29-bar chart at width: '300px' renders 3 x-axis labels on main (1, 15, 29) and 29 labels with this patch, all packed into roughly 250px of plot area.

This is worth calling out because the behaviour requested in the issue was ticks at a regular interval, widening the stride as bars get denser (every bar, then every 2, then every 5, then every 10). This PR implements the half that fixes alignment and drops the half that keeps the axis legible.

2. Fractional x values now produce runs of duplicate labels

12 bars at x = 0.25, 0.50, ... 3.00 at width: '600px':

  • main: 0, 1, 2, 2, 3
  • this PR: 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3

The tick count assumes the x values are one unit apart, so when the spacing is below 1 the default numeric formatter rounds several ticks to the same string. That is the same failure the guard immediately below is written to prevent:

// this check is for when ticks exceeds total datapoints and that would result in duplicate labels

but that guard only compares ticks against dataPoints, so it cannot see the spacing.

Root cause

Both of the above come from the same modelling assumption: the change treats "number of data points" as if it were "width of the x range". That is also why it does not help sparse data. A bar chart with [{x:1},{x:5},{x:100},{x:3000}] still gives [1, 1000.67, 2000.34, 3000.01] with the patch (vs [1, 750.75, 1500.5, 2250.25, 3000] on main). Neither is integer-valued and no tick lands on a bar, so the "every tick lands on an integer position aligned with a bar" claim only holds when the x values happen to be consecutive integers.

Suggested direction

Keep the integer-step goal, but pick a stride rather than forcing stride 1. Something along the lines of: take the width budget the code already computes (Math.round(gl.svgWidth / 150)), then choose the smallest stride from 1, 2, 5, 10, ... such that dataPoints / stride fits that budget, and derive ticks from that. This preserves bar alignment at any width, degrades gracefully on narrow charts, and matches what the issue asked for. Additionally, gating the block on x values that are actually integer and evenly spaced would close the fractional-label case.

Smaller notes

  • The new block duplicates the ticks = gl.dataPoints - 1 assignment ten lines above. The underlying asymmetry is that bar charts leave config.xaxis.type at 'category' (with convertedCatToNumeric: false) while line charts get promoted to 'numeric', which is why the existing branch does not fire for bar. Folding the bar case into that existing condition, or fixing the promotion, would avoid two copies of the heuristic that have to stay in sync.
  • tests/unit/xaxis.spec.js: the 25-bar test uses Math.random() for the y values, but no assertion reads y. Worth using a fixed sequence so a future failure is reproducible.
  • Please also add coverage for the two cases above (a narrow chart, and non-integer x spacing) once the approach settles.

For what it is worth, a few things I checked that are not problems: the xTickAmount change on datetime bar charts (3 to 24) produces identical rendered labels because datetime goes through the timescale path; horizontal bars are correctly excluded from the Grid.js code that consumes xTickAmount; and the xTickAmount: -1 on an empty series is pre-existing on main, not introduced here.

Happy to take another look once the stride handling is in.

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.

x axis ticks are misaligned in bar charts that have more than 10 ticks

2 participants