From 21c779d91dde52851dd8d5e2538525fc4f5dcb0a Mon Sep 17 00:00:00 2001 From: Jayesh Date: Sun, 9 Aug 2026 19:16:01 +0530 Subject: [PATCH 1/2] Reset forced minimum tick spacing on every calc pass setConvert cleared ax._minDtick / ax._forceTick0 so each calc pass would start over, but the cleanup never had any effect: setConvert runs while supplyDefaults builds the new _fullLayout, where the keys do not exist yet, and relinkPrivateKeys then copies the old values back onto it. Axes.minDtick treats 0 as "forcing cancelled", so the 0 written by whichever figure was drawn first survived every later update and vetoed the forcing for every figure after it. Reacting from a scatter to a box plot lost the one-tick-per-box spacing, while newPlot of the same figure kept it. Move the reset into ax.clearCalc, the axis' own per-calc-pass reset, which doCalcdata runs for every axis before any cross-trace calc. --- src/plots/cartesian/set_convert.js | 12 +++++---- test/jasmine/tests/axes_test.js | 39 +++++++++++++++++++++++++++++ test/jasmine/tests/plot_api_test.js | 3 ++- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index cc3c6ccf7ee..ef3d7ae74ca 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -58,7 +58,7 @@ function isValidCategory(v) { * Creates/updates these conversion functions, and a few more utilities * like cleanRange, and makeCalcdata * - * also clears ._minDtick, ._forceTick0 + * also creates ax.clearCalc, which clears ._minDtick, ._forceTick0 */ module.exports = function setConvert(ax, fullLayout) { fullLayout = fullLayout || {}; @@ -952,6 +952,12 @@ module.exports = function setConvert(ax, fullLayout) { // should skip if not category nor multicategory ax.clearCalc = function() { + // for bar charts and box plots: reset forced minimum tick spacing. + // this has to happen here rather than in setConvert, as the values + // are relinked onto the new fullLayout after supplyDefaults runs + delete ax._minDtick; + delete ax._forceTick0; + var group = ax._matchGroup; if(group) { var categories = null; @@ -1024,8 +1030,4 @@ module.exports = function setConvert(ax, fullLayout) { // even though it won't be needed by this axis ax._separators = fullLayout.separators; ax._numFormat = locale ? locale.numberFormat : numberFormat; - - // and for bar charts and box plots: reset forced minimum tick spacing - delete ax._minDtick; - delete ax._forceTick0; }; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 15eca358dfe..df78b999bf0 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -8303,6 +8303,45 @@ describe('more react tests', function() { expect(gd._fullLayout.xaxis.range).toBeCloseToArray([-0.173, 2]); }).then(done, done.fail); }); + + it('should not carry over the forced minimum tick spacing of the previous figure', function(done) { + var layout = {width: 700, height: 400}; + + var scatterFig = { + data: [{y: [1, 2, 3]}], + layout: layout + }; + + // one box per integer position - each box forces a tick of its own + var boxFig = { + data: [{ + type: 'box', + x: [1, 1, 2, 2, 3, 3], + y: [1, 2, 3, 4, 5, 6] + }], + layout: layout + }; + + function getXLabels() { + return gd._fullLayout.xaxis._vals.map(function(d) { return d.text; }); + } + + Plotly.newPlot(gd, boxFig) + .then(function() { + expect(getXLabels()).toEqual(['1', '2', '3']); + + // scatter cancels the forcing for its own figure only + return Plotly.newPlot(gd, scatterFig); + }) + .then(function() { + return Plotly.react(gd, boxFig); + }) + .then(function() { + expect(gd._fullLayout.xaxis._minDtick).toBe(1); + expect(getXLabels()).toEqual(['1', '2', '3']); + }) + .then(done, done.fail); + }); }); describe('category preservation tests on gd passed to Plotly.react()', function() { diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index d962aab7e1b..0a9e00cea51 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -1649,7 +1649,8 @@ describe('Test plot api', function () { return Plotly.restyle(gd, { x0: 12.3 }); }) .then(function () { - checkTicks('x', ['12', '12.5'], 'switched to numeric'); + // a single box forces one tick at its own position + checkTicks('x', ['12.3'], 'switched to numeric'); expect(gd._fullLayout.xaxis.type).toBe('linear'); }) .then(done, done.fail); From b4ec95cd79de969cfa1dfeab00ac9486f7347203 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Sun, 9 Aug 2026 19:17:44 +0530 Subject: [PATCH 2/2] Add draftlog for #7950 --- draftlogs/7950_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7950_fix.md diff --git a/draftlogs/7950_fix.md b/draftlogs/7950_fix.md new file mode 100644 index 00000000000..0326814ba32 --- /dev/null +++ b/draftlogs/7950_fix.md @@ -0,0 +1 @@ + - Fix box, violin, candlestick and ohlc traces losing their forced minimum tick spacing when a graph div is updated in place with `Plotly.react` or `Plotly.restyle` [[#7950](https://github.com/plotly/plotly.js/pull/7950)]