From 1b57031ad3ba637fc85c0407e3f82118997ab668 Mon Sep 17 00:00:00 2001 From: Roshan Banisetti Date: Fri, 23 Oct 2026 00:57:28 -0600 Subject: [PATCH 1/2] fix: Clear a stale _inputRange when Plotly.react or a whole axis object sets a new range On a constrain:'domain' axis, constraints.enforce compares the range with _inputRange and puts _inputRange back when the range is smaller. Plotly.react and Plotly.relayout with a whole axis object kept the old _inputRange, so a new range drew as the previous one. diffLayout now clears it the way it clears _inputDomain, and the whole axis object case does what the dotted range keys already do. --- src/plot_api/plot_api.js | 17 ++++++++++ test/jasmine/tests/axes_test.js | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 5af0b8951fa..5eea5fc4806 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2112,6 +2112,12 @@ function _relayout(gd, aobj) { if (!newType || newType === '-') newType = 'linear'; Registry.getComponentMethod('annotations', 'convertCoords')(gd, fullProp, newType, doextra); Registry.getComponentMethod('images', 'convertCoords')(gd, fullProp, newType, doextra); + + // a range or autorange in a whole axis object is handled like the dotted keys above + if (!ptrunk && fullProp && vi && (vi.range !== undefined || vi.autorange !== undefined)) { + recordAlteredAxis(`${pleaf}.range`); + fullProp._inputRange = null; + } } // alter gd.layout @@ -2872,6 +2878,17 @@ function diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition) { // because it's likely that if the newDomain and oldDomain are the same, the user // passed in the same layout object and we should keep the _inputDomain. } + + // Same for _inputRange: constraints compare the range against it, so a stale one + // puts the previous range back + const newRange = newFullLayout[key].range; + const oldRange = oldFullLayout[key].range; + const oldInputRange = oldFullLayout[key]._inputRange; + if (oldInputRange && newRange && oldRange) { + const isInputRange = newRange[0] === oldInputRange[0] && newRange[1] === oldInputRange[1]; + const isOldRange = newRange[0] === oldRange[0] && newRange[1] === oldRange[1]; + if (!isInputRange && !isOldRange) newFullLayout[key]._inputRange = null; + } } var diffOpts = { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index e964c3c33da..45fa89d6f93 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1884,6 +1884,61 @@ describe('Test axes', function() { .then(done, done.fail); }); + describe('a new range on a constrain:domain axis', () => { + const data = () => [{ x: [0, 20, 50, 80, 100], y: [0, 20, 50, 80, 100] }]; + const layout = (xRange) => ({ + width: 400, + height: 400, + margin: { l: 0, r: 0, t: 0, b: 0 }, + xaxis: { range: xRange, constrain: 'domain' }, + yaxis: { range: [0, 100], scaleanchor: 'x', scaleratio: 1 } + }); + + it('is applied by Plotly.react', (done) => { + Plotly.newPlot(gd, data(), layout([0, 100])) + .then(() => Plotly.react(gd, data(), layout([20, 80]))) + .then(() => { + assertRangeDomain('xaxis', [20, 80], [0, 1], [0.2, 0.8]); + expect(gd._fullLayout.xaxis._inputRange).toEqual([20, 80]); + }) + .then(done, done.fail); + }); + + it('is applied by Plotly.relayout with a whole axis object', (done) => { + Plotly.newPlot(gd, data(), layout([0, 100])) + .then(() => Plotly.relayout(gd, { xaxis: layout([20, 80]).xaxis, yaxis: layout().yaxis })) + .then(() => { + assertRangeDomain('xaxis', [20, 80], [0, 1], [0.2, 0.8]); + expect(gd._fullLayout.xaxis._inputRange).toEqual([20, 80]); + }) + .then(done, done.fail); + }); + + [ + ['dotted keys', { 'xaxis.range': [20, 80] }], + ['a whole axis object', { xaxis: layout([20, 80]).xaxis }] + ].forEach(([name, update]) => { + it(`lets the other axis rescale when Plotly.relayout sets one range with ${name}`, (done) => { + Plotly.newPlot(gd, data(), layout([0, 100])) + .then(() => Plotly.relayout(gd, update)) + .then(() => { + assertRangeDomain('xaxis', [20, 80], [0, 1], [0, 1]); + assertRangeDomain('yaxis', [20, 80], [0, 1], [0, 1]); + }) + .then(done, done.fail); + }); + }); + + it('keeps the drawn range when Plotly.react gets the same layout again', (done) => { + Plotly.newPlot(gd, data(), layout([20, 80])) + .then(() => Plotly.react(gd, gd.data, gd.layout)) + .then(() => { + assertRangeDomain('xaxis', [20, 80], [0, 1], [0.2, 0.8]); + }) + .then(done, done.fail); + }); + }); + it('can change per-axis constrain:domain/range and constraintoward', function(done) { Plotly.newPlot(gd, // start with a heatmap as it has no padding so calculations are easy From dc4dfd159982eb4c4fc9f50fa8a3c068032c7676 Mon Sep 17 00:00:00 2001 From: Roshan Banisetti Date: Fri, 23 Oct 2026 00:58:57 -0600 Subject: [PATCH 2/2] Add draftlog for #8144 --- draftlogs/8144_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/8144_fix.md diff --git a/draftlogs/8144_fix.md b/draftlogs/8144_fix.md new file mode 100644 index 00000000000..94d442aa96b --- /dev/null +++ b/draftlogs/8144_fix.md @@ -0,0 +1 @@ + - Fix `Plotly.react` and `Plotly.relayout` with a whole axis object ignoring a new range on an axis with `constrain: 'domain'` [[#8144](https://github.com/plotly/plotly.js/pull/8144)]