fix: align x-axis ticks with bar positions for numeric bar charts with >10 bars (#5086) - #5259
fix: align x-axis ticks with bar positions for numeric bar charts with >10 bars (#5086)#5259waterWang wants to merge 1 commit into
Conversation
…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
left a comment
There was a problem hiding this comment.
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 labelsbut 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 - 1assignment ten lines above. The underlying asymmetry is that bar charts leaveconfig.xaxis.typeat'category'(withconvertedCatToNumeric: 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 usesMath.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.
What kind of change does this PR introduce?
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 fromMath.round(svgWidth / 150). With 11+ bars this can produce a non-integer step (e.g. 11 bars over a 600px chart →ticks=4→step=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?
src/modules/Range.js, when the chart is a vertical bar chart with numeric x-axis data (isXNumeric) anddataPoints < 30, setsticks = dataPoints - 1so the scale uses integer steps.Screenshots (if appropriate)
N/A — verified via unit tests that
xAxisScale.resultproduces integer-aligned ticks.Other information
All 2140 unit tests pass (94 test files).