You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
dmlvr
changed the title
Chart scrollbar internal bug 26 2
Chart - ScrollBar: the thumb jumps to the beginning of the bar when the argument axis has scale breaks
Sep 15, 2026
Datetime TimeIntervalConfig accepts numeric millisecond intervals, and forceUserTickInterval preserves such a number in _tickInterval. dateUtils.dateToMilliseconds(number) returns 0, so these new range-length calculations treat a user break as fully hidden instead of retaining one tick interval; the scrollbar thumb and panning scale are incorrect for datetime axes with a numeric tickInterval. Return numeric intervals unchanged and only convert interval objects.
adjustPannedRange receives zoom.min/zoom.max directly, but Translator2D.zoom() returns them in screen order for an inverted continuous axis, so startValue can be greater than endValue. _getBreaksForRange does not normalize viewport bounds and _filterBreaks then finds no breaks, causing this early return to skip the scale-preserving correction. Panning or dragging the scrollbar across a break on an inverted axis can therefore change the visible duration; normalize the range before break detection and correction.
wholeRangeBreaks carries cumulativeWidth values computed from the axis breakStyle.width, but this translator is configured with breaksSize: 0. Translator2D still uses cumulativeWidth when calculating its ratio and converting positions, so user-defined breaks leave a gap in the scrollbar track and make the thumb length differ from the axis visible-range fraction. Clear or recompute cumulativeWidth for the scrollbar-specific break list before passing it to the translator.
This correction is skipped as soon as the newly computed range no longer contains a break. If a pan starts with a break in the viewport and then moves past that break, the early return leaves the data span changed, so the chart zooms out/in instead of preserving the original visible scale. The condition should also consider breaks in storedParams.startRange (or run the correction for every continuous range whose starting range had a break).
if (!storedParams || type === constants.discrete || type === constants.logarithmic
|| !this._getBreaksForRange(range.startValue, range.endValue).length) {
return range;
This ordering check is not valid for discrete axes: from() returns categories in axis order, but > compares the category values themselves. For example, a non-inverted axis with categories ['B', 'A'] produces { startValue: 'A', endValue: 'B' }, and adjustRange does not normalize discrete ranges, so dragging the scrollbar can reverse the visible category range. Determine the order from the translator's inversion state (or the category indices), rather than comparing values.
return from > to ? { startValue: to, endValue: from } : { startValue: from, endValue: to };
The end handler applies the explicit scrollbar range without checking whether argument-axis panning is enabled. Thus, even if the move branch is guarded, releasing a scrollbar drag still pans once for charts configured with argumentAxis: 'zoom' or none; retain the same zoomAndPan.options.argumentAxis.pan condition here.
if (e.scrollRange) {
panArgumentAxisToThumb(e, e.scrollRange);
}
from and to are category values for a discrete translator, so comparing them with > does not describe their order on the axis. For categories such as ['Z', 'A', 'M'], this reverses the range emitted by every real scrollbar drag and the new scrollRange path can pan to the wrong categories. Use the translator's screen/data direction to choose the endpoints instead of comparing category values.
// on an inverted axis the bar runs against the data, so the ends are reported in data order
return from > to ? { startValue: to, endValue: from } : { startValue: from, endValue: to };
The scrollbar range is normalized to data order above, but on an inverted axis the physical start of the thumb corresponds to scrollRange.endValue, not startValue. Passing the hard-coded 'start' anchor makes the scale-break correction preserve the wrong edge, so dragging an inverted scrollbar across a break can shift or resize the range instead of following the thumb. Select 'end' when the axis translator is inverted.
This path now handles every real scrollbar drag, but it bypasses the options.argumentAxis.pan check used by the regular scrollbar path. When zoomAndPan.argumentAxis is "zoom" or "none", the start handler only marks the event as cancelled; the move handler still calls axisZoom for every argument axis and pans the chart. Guard this path with the pan option so dragging a scrollbar cannot enable panning that the option disables.
This manual half-tick correction does not match the axis normalization: tick_generator snaps each user-break boundary to a nearby tick via getBaseTick before applying the ±half-interval correction, while this code shifts the raw boundaries directly. For breaks not aligned to the tick grid, the scrollbar hides a different interval from the chart axis, so thumb geometry and panning scale diverge. Reuse the normalized break values or centralize that normalization.
return this._getBreaksForRange(businessRange.min, businessRange.max)
.reduce((result, scaleBreak) => {
const hidden = this._getHiddenDuration(scaleBreak, interval);
// a gap is hidden whole, so its shift is zero and it is taken as is
const shift = ((scaleBreak.to - scaleBreak.from) - hidden) / 2;
For a discrete argument axis, init passes stick: false when valueMarginsEnabled is at its default, so setPosition deliberately places the thumb's end at the boundary after the last visible category. Calling translator.from(start + length) with the default direction 0 converts that boundary to the next category (for example, a visible range [3, 7] becomes [3, 8]), expanding the range on every real scrollbar drag. Map each endpoint with the corresponding direction/offset used by setPosition (including the inverted case), and add a discrete scrollbar-drag regression test.
const from = translator.from(start);
const to = translator.from(start + length);
The new scrollRange branch is only covered by scrollbar tests using workdaysOnly. The user-defined-break cases exercise programmatic range changes and the ordinary chart pointer, but never drag the real scrollbar across argumentAxis.breaks; a regression in translating a user-defined whole-range break to scrollRange would therefore pass. Please add a direct scrollbar-drag case with a user-defined break and assert the range/scale after crossing it.
if (e.scrollRange && options.argumentAxis.pan) {
panArgumentAxisToThumb(e, e.scrollRange);
This remaps the axis from the scrollbar position a second time on every drag end, including a start/end gesture with no move. When the visual range boundary is inside a zero-width scale break, _getRangeAtPosition resolves that boundary to the value after the break, so simply clicking the thumb can change the chart range even though the thumb did not move. Apply the thumb range only for an actual move (or preserve the current range for a zero-offset end event).
if (e.scrollRange && options.argumentAxis.pan) {
panArgumentAxisToThumb(e, e.scrollRange);
}
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Whole-range break calculations use raw option breaks instead of tick-normalized breaks, causing incorrect scrollbar sizing or panning for some numeric and non-aligned breaks.
Get a fresh assessment by requesting another Copilot review.
The corresponding early return here also makes adjustPannedRange treat a logarithmic range as if it had no breaks. Even if the scrollbar is taught about the whole-range breaks above, panning across a supported logarithmic break will not preserve the visible scale, so this path needs the same log-space break-length handling.
This guard skips all breaks for logarithmic axes, even though the axis translator and tick generator support logarithmic scale breaks and argumentAxis.breaks is a public option. A logarithmic argument axis with breaks therefore still builds a break-free scrollbar translator and can retain the jump this change is intended to fix. Please handle break lengths in the logarithmic coordinate space instead of excluding this axis type.
if (type === constants.discrete || type === constants.logarithmic
|| !isDefined(businessRange.min) || !isDefined(businessRange.max)) {
return [];
For a non-inverted axis this always anchors the corrected range at its startValue. If the thumb is dragged to the trailing whole-range bound while its raw range intersects a break, adjustPannedRange may shorten the range by moving endValue backward; the end is no longer at getZoomBounds().endValue, so the thumb leaves a gap at the end and the last window cannot be reached. The correction needs to switch to the opposite anchor when the raw range is at the trailing bound (and symmetrically at the leading bound).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.