From b121bc921907b1268f8c5d4d4dbb86e19e14e1f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 17:40:41 +0000 Subject: [PATCH 1/3] Clamp colorbar axis domain so it can never invert Vertical colorbars could end up with ax.domain[1] < ax.domain[0], producing a negative axis length and throwing "Something went wrong with axis scaling" during draw. This happened in two spots: - the initial domain assignment, when the colorbar's available height (len * plot height) is smaller than 2 * ypad - the title-height adjustment, when the title (top or bottom side) is taller than the colorbar's domain All three assignments are now clamped so domain[1] can never drop below domain[0] (or domain[0] rise above domain[1]). Fixes #6499 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011XRrkTKVes3TEaB8FNDrea --- src/components/colorbar/draw.js | 8 ++--- test/jasmine/tests/colorbar_test.js | 51 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index 3fe5a5e9ce3..df9f220389c 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -302,10 +302,10 @@ function drawColorBar(g, opts, gd) { // allow it outside [0,1] ax.domain = isVertical ? [ vFrac + ypad / gs.h, - vFrac + lenFrac - ypad / gs.h + Math.max(vFrac + lenFrac - ypad / gs.h, vFrac + ypad / gs.h) ] : [ vFrac + xpad / gs.w, - vFrac + lenFrac - xpad / gs.w + Math.max(vFrac + lenFrac - xpad / gs.w, vFrac + xpad / gs.w) ]; ax.setScale(); @@ -473,10 +473,10 @@ function drawColorBar(g, opts, gd) { titleHeight += 5; if(titleSide === 'top') { - ax.domain[1] -= titleHeight / gs.h; + ax.domain[1] = Math.max(ax.domain[1] - titleHeight / gs.h, ax.domain[0]); titleTrans[1] *= -1; } else { - ax.domain[0] += titleHeight / gs.h; + ax.domain[0] = Math.min(ax.domain[0] + titleHeight / gs.h, ax.domain[1]); var nlines = svgTextUtils.lineCount(titleText); titleTrans[1] += (1 - nlines) * lineSize; } diff --git a/test/jasmine/tests/colorbar_test.js b/test/jasmine/tests/colorbar_test.js index 99fa90e5e5b..6dbd1042b93 100644 --- a/test/jasmine/tests/colorbar_test.js +++ b/test/jasmine/tests/colorbar_test.js @@ -95,6 +95,57 @@ describe('Test colorbar:', function() { .then(done, done.fail); }); + // see https://github.com/plotly/plotly.js/issues/6499 + it('does not throw when the colorbar is too short for its own length/padding', function(done) { + Plotly.newPlot(gd, [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + color: [1, 2, 3], + colorbar: {len: 0.01, thickness: 30} + } + }], {width: 300, height: 120}) + .then(done, done.fail); + }); + + it('does not throw when a top-side colorbar title is taller than the colorbar', function(done) { + Plotly.newPlot(gd, [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + color: [1, 2, 3], + colorbar: { + title: {text: 'Long title', side: 'top', font: {size: 60}}, + len: 0.3, + thickness: 30 + } + } + }], {width: 300, height: 120}) + .then(done, done.fail); + }); + + it('does not throw when a bottom-side colorbar title is taller than the colorbar', function(done) { + Plotly.newPlot(gd, [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + color: [1, 2, 3], + colorbar: { + title: {text: 'Long title', side: 'bottom', font: {size: 60}}, + len: 0.3, + thickness: 30 + } + } + }], {width: 300, height: 120}) + .then(done, done.fail); + }); + function assertCB(msg, present, opts) { var expandedMarginR = opts.expandedMarginR; var expandedMarginT = opts.expandedMarginT; From 4d427c7125ee43cfd4aabd382f21b5147025c3f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 17:48:18 +0000 Subject: [PATCH 2/3] Add draftlog entry for PR #7908 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011XRrkTKVes3TEaB8FNDrea --- draftlogs/7908_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7908_fix.md diff --git a/draftlogs/7908_fix.md b/draftlogs/7908_fix.md new file mode 100644 index 00000000000..955c79cea16 --- /dev/null +++ b/draftlogs/7908_fix.md @@ -0,0 +1 @@ + - Fix crash ("Something went wrong with axis scaling") when a colorbar's title or padding leaves it with a negative domain length [[#7908](https://github.com/plotly/plotly.js/pull/7908)] From 7011939c1d80889f28d13afa7d4925cf74607370 Mon Sep 17 00:00:00 2001 From: Sergio Oller Date: Fri, 31 Jul 2026 02:43:54 +0000 Subject: [PATCH 3/3] Address review feedback: extract domain-clamping helpers Factor the repeated "clamp domain endpoint without crossing the other bound" logic into two small helpers, insetDomainStart and insetDomainEnd, and use them at all four call sites instead of inline Math.max/Math.min. This also fixes a spot the review caught that the original patch missed: the horizontal colorbar's title.side === 'right' branch had the same unclamped-domain bug as the vertical title branches, just never hit by the reported repro. Added a regression test for it alongside the existing three. AI-assistant: Claude Sonnet 5 --- src/components/colorbar/draw.js | 21 +++++++++++---------- test/jasmine/tests/colorbar_test.js | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index df9f220389c..5fbf919d3ae 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -167,6 +167,12 @@ function makeColorBarData(gd) { return out; } +// Move domain[0] toward domain[1] by delta, without crossing it +const insetDomainStart = (domain, delta) => [Math.min(domain[0] + delta, domain[1]), domain[1]]; + +// Move domain[1] toward domain[0] by delta, without crossing it +const insetDomainEnd = (domain, delta) => [domain[0], Math.max(domain[1] - delta, domain[0])]; + function drawColorBar(g, opts, gd) { var isVertical = opts.orientation === 'v'; var len = opts.len; @@ -300,13 +306,8 @@ function drawColorBar(g, opts, gd) { // set domain after init, because we may want to // allow it outside [0,1] - ax.domain = isVertical ? [ - vFrac + ypad / gs.h, - Math.max(vFrac + lenFrac - ypad / gs.h, vFrac + ypad / gs.h) - ] : [ - vFrac + xpad / gs.w, - Math.max(vFrac + lenFrac - xpad / gs.w, vFrac + xpad / gs.w) - ]; + var padFrac = isVertical ? ypad / gs.h : xpad / gs.w; + ax.domain = insetDomainEnd(insetDomainStart([vFrac, vFrac + lenFrac], padFrac), padFrac); ax.setScale(); @@ -473,10 +474,10 @@ function drawColorBar(g, opts, gd) { titleHeight += 5; if(titleSide === 'top') { - ax.domain[1] = Math.max(ax.domain[1] - titleHeight / gs.h, ax.domain[0]); + ax.domain = insetDomainEnd(ax.domain, titleHeight / gs.h); titleTrans[1] *= -1; } else { - ax.domain[0] = Math.min(ax.domain[0] + titleHeight / gs.h, ax.domain[1]); + ax.domain = insetDomainStart(ax.domain, titleHeight / gs.h); var nlines = svgTextUtils.lineCount(titleText); titleTrans[1] += (1 - nlines) * lineSize; } @@ -487,7 +488,7 @@ function drawColorBar(g, opts, gd) { } else { // horizontal colorbars if(titleWidth) { if(titleSide === 'right') { - ax.domain[0] += (titleWidth + titleFontSize / 2) / gs.w; + ax.domain = insetDomainStart(ax.domain, (titleWidth + titleFontSize / 2) / gs.w); } titleGroup.attr('transform', strTranslate(titleTrans[0], titleTrans[1])); diff --git a/test/jasmine/tests/colorbar_test.js b/test/jasmine/tests/colorbar_test.js index 6dbd1042b93..1785f8cb21c 100644 --- a/test/jasmine/tests/colorbar_test.js +++ b/test/jasmine/tests/colorbar_test.js @@ -146,6 +146,25 @@ describe('Test colorbar:', function() { .then(done, done.fail); }); + it('does not throw when a right-side horizontal colorbar title is wider than the colorbar', function(done) { + Plotly.newPlot(gd, [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + color: [1, 2, 3], + colorbar: { + orientation: 'h', + title: {text: 'Long title', side: 'right', font: {size: 60}}, + len: 0.3, + thickness: 30 + } + } + }], {width: 300, height: 120}) + .then(done, done.fail); + }); + function assertCB(msg, present, opts) { var expandedMarginR = opts.expandedMarginR; var expandedMarginT = opts.expandedMarginT;