From 4f438205f3c5b972092d47aaaafafdcaafbd50d5 Mon Sep 17 00:00:00 2001 From: Robert Collar Date: Tue, 5 May 2026 21:36:22 -0400 Subject: [PATCH] dragbox: sync ax._input.range when mutating ax.range during pan and scroll-zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scaleZoom() consistently writes both `ax.range` and `ax._input.range` (scale_zoom.js:13), but several mutation sites in dragbox.js only wrote `ax.range`: * zoomWheelOneAxis (mousewheel zoom) * dz (corner-handle zoom on drag) * updateMatchedAxRange (linked-axis update during drag) * dragAxList (pan) `ax._input.range` is what `gd.layout..range` references. Leaving it stale means user code reading `gd.layout` from a `plotly_relayouting` handler sees the *previous* range while `gd._fullLayout` already reflects the in-progress gesture. Any work that handler does (recomputing positions, fetching new tiles, updating annotations) is therefore computed against an out-of-date viewport, and only "catches up" once dragTail eventually runs a full relayout. This is reproducible by attaching a relayouting handler that compares gd.layout.xaxis.range to gd._fullLayout.xaxis.range during a wheel zoom - before the fix the two diverge; after the fix they stay in sync. The change is the minimal extension of the existing `ax.range = …` assignments to also set `ax._input.range`, matching the pattern already used by scaleZoom and Plotly.relayout. --- src/plots/cartesian/dragbox.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 0aa0caceee2..123edb2efce 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -499,7 +499,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { var axRange = Lib.simpleMap(ax.range, ax.r2l); var v0 = axRange[0] + (axRange[1] - axRange[0]) * centerFraction; function doZoom(v) { return ax.l2r(v0 + (v - v0) * zoom); } - ax.range = axRange.map(doZoom); + ax.range = ax._input.range = axRange.map(doZoom); } if(editX) { @@ -613,7 +613,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // if l2r comes back false or undefined, it means we've dragged off // the end of valid ranges - so stop. - if(newEnd !== false && newEnd !== undefined) axi.range[end] = newEnd; + if(newEnd !== false && newEnd !== undefined) { + axi.range[end] = newEnd; + // keep ax._input.range (the user-facing layout) in sync so + // plotly_relayouting handlers reading gd.layout see fresh values + axi._input.range = axi.range.slice(); + } } return movedAx._length * (movedAx._rl[end] - newLinearizedEnd) / (movedAx._rl[end] - movedAx._rl[otherEnd]); @@ -710,7 +715,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { out[ax._name + '.range[0]'] = out[ax2._name + '.range[0]']; out[ax._name + '.range[1]'] = out[ax2._name + '.range[1]']; } else { - ax.range = ax2.range.slice(); + ax.range = ax._input.range = ax2.range.slice(); } } } @@ -1120,12 +1125,12 @@ function dragAxList(axList, pix) { var d1 = axi.p2l(p1 + pix) - axi.p2l(p1); var delta = (d0 + d1) / 2; - axi.range = [ + axi.range = axi._input.range = [ axi.l2r(axi._rl[0] - delta), axi.l2r(axi._rl[1] - delta) ]; } else { - axi.range = [ + axi.range = axi._input.range = [ axi.l2r(axi._rl[0] - pix / axi._m), axi.l2r(axi._rl[1] - pix / axi._m) ];