From 16d93d86c26cb9472b47b91cc0f53ded13d255b9 Mon Sep 17 00:00:00 2001 From: Otso Lehdonvirta Date: Tue, 29 Sep 2026 08:36:26 +0200 Subject: [PATCH 1/2] Fix directional axis automargin anchors Preserve anchor coordinates when filtering margin directions so enabled right and top margins can grow. Add regression coverage for anchored and free axes, disabled margins, and clipped labels. Refs #8073 --- draftlogs/LOCAL_PR_fix.md | 1 + src/plots/cartesian/axes.js | 5 ++- test/jasmine/tests/axes_test.js | 67 +++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 draftlogs/LOCAL_PR_fix.md diff --git a/draftlogs/LOCAL_PR_fix.md b/draftlogs/LOCAL_PR_fix.md new file mode 100644 index 00000000000..7bad6316862 --- /dev/null +++ b/draftlogs/LOCAL_PR_fix.md @@ -0,0 +1 @@ +- Fix directional axis automargins that discard anchor coordinates and clip labels [[#LOCAL_PR](https://github.com/plotly/plotly.js/pull/LOCAL_PR)] diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 7beca25c086..98f1e097a4c 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -3015,12 +3015,13 @@ axes.drawOne = function(gd, ax, opts) { function filterPush(push, automargin) { if(!push) return; - var keepMargin = Object.keys(MARGIN_MAPPING).reduce(function(data, nextKey) { + // Anchor coordinates locate enabled margin constraints. They do not enable margin growth. + const keepMargin = Object.keys(MARGIN_MAPPING).reduce((data, nextKey) => { if(automargin.indexOf(nextKey) !== -1) { MARGIN_MAPPING[nextKey].forEach(function(key) { data[key] = 1;}); } return data; - }, {}); + }, { x: 1, y: 1 }); Object.keys(push).forEach(function(key) { if(!keepMargin[key]) { if(key.length === 1) push[key] = 0; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index e964c3c33da..68ec9561920 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -4687,6 +4687,73 @@ describe('Test axes', function() { .then(done, done.fail); }); + ['right', 'left', 'top', 'bottom'].forEach((side) => { + [false, true].forEach((free) => { + it(`preserves ${side} anchors for ${free ? 'free' : 'anchored'} partial automargin`, async () => { + const vertical = side === 'right' || side === 'left'; + const axis = vertical ? 'yaxis' : 'xaxis'; + const id = vertical ? 'y' : 'x'; + const edge = side[0]; + const fraction = side === 'right' || side === 'top' ? 1 : 0; + const layout = { + width: 500, + height: 300, + margin: { l: 0, r: 0, t: 0, b: 0 }, + xaxis: { range: [0, 4], visible: !vertical }, + yaxis: { range: [0, 4], visible: vertical } + }; + layout[axis] = { + range: [0, 4], + side, + automargin: true, + tickvals: [1, 2, 3], + ticktext: ['Long label', 'Long label', 'Long label'], + tickangle: 0, + ...(free ? { anchor: 'free', position: fraction } : {}) + }; + await Plotly.newPlot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }], layout); + const automatic = gd._fullLayout._size[edge]; + expect(automatic).toBeGreaterThan(0); + + for (const flag of [side, vertical ? 'width' : 'height']) { + await Plotly.relayout(gd, { [`${axis}.automargin`]: flag }); + expect(gd._fullLayout._size[edge]).toBe(automatic); + expect(gd._fullLayout._pushmargin[`${id}.automargin`][edge].val).toBe(fraction); + for (const other of ['l', 'r', 't', 'b'].filter((value) => value !== edge)) { + expect(gd._fullLayout._size[other]).toBe(0); + } + } + + await Plotly.relayout(gd, { [`${axis}.automargin`]: vertical ? 'height' : 'width' }); + expect(gd._fullLayout._size[edge]).toBe(0); + await Plotly.relayout(gd, { [`${axis}.automargin`]: true }); + expect(gd._fullLayout._size[edge]).toBe(automatic); + await Plotly.relayout(gd, { [`${axis}.automargin`]: false }); + expect(gd._fullLayout._size[edge]).toBe(0); + }); + }); + }); + + it('keeps right-side labels inside the paper when left automargin is excluded', async () => { + await Plotly.newPlot(gd, [{ x: [0, 1, 2], y: [1000, 2000, 3000] }], { + width: 500, + height: 300, + margin: { l: 40, r: 0, t: 30, b: 40 }, + yaxis: { side: 'right', automargin: 'right+top+bottom', tickformat: '.0f' } + }); + const rightMargin = gd._fullLayout._size.r; + expect(rightMargin).toBeGreaterThan(0); + expect(gd._fullLayout._size.l).toBe(40); + const paper = gd.querySelector('.main-svg').getBoundingClientRect(); + const labels = gd.querySelectorAll('.ytick text'); + expect(labels.length).toBeGreaterThan(0); + labels.forEach((label) => { + expect(label.getBoundingClientRect().right).toBeLessThanOrEqual(paper.right + 0.5); + }); + await Plotly.relayout(gd, { 'yaxis.automargin': true }); + expect(gd._fullLayout._size.r).toBe(rightMargin); + }); + it('should handle partial automargin', function(done) { var initialSize; From 309de6b3993f7d6c0bbbeda6fae14ca0973228db Mon Sep 17 00:00:00 2001 From: Otso Lehdonvirta Date: Tue, 29 Sep 2026 09:48:33 +0200 Subject: [PATCH 2/2] Add PR number to automargin draftlog --- draftlogs/8090_fix.md | 1 + draftlogs/LOCAL_PR_fix.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 draftlogs/8090_fix.md delete mode 100644 draftlogs/LOCAL_PR_fix.md diff --git a/draftlogs/8090_fix.md b/draftlogs/8090_fix.md new file mode 100644 index 00000000000..d3dfe48236a --- /dev/null +++ b/draftlogs/8090_fix.md @@ -0,0 +1 @@ +- Fix directional axis automargins that discard anchor coordinates and clip labels [[#8090](https://github.com/plotly/plotly.js/pull/8090)] diff --git a/draftlogs/LOCAL_PR_fix.md b/draftlogs/LOCAL_PR_fix.md deleted file mode 100644 index 7bad6316862..00000000000 --- a/draftlogs/LOCAL_PR_fix.md +++ /dev/null @@ -1 +0,0 @@ -- Fix directional axis automargins that discard anchor coordinates and clip labels [[#LOCAL_PR](https://github.com/plotly/plotly.js/pull/LOCAL_PR)]