Skip to content

Fix bogus colorbar size rescale when restyling to the mode the colorbar already uses - #8015

Draft
LeonxLJX wants to merge 1 commit into
plotly:mainfrom
LeonxLJX:fix-colorbar-mode-conversion
Draft

Fix bogus colorbar size rescale when restyling to the mode the colorbar already uses#8015
LeonxLJX wants to merge 1 commit into
plotly:mainfrom
LeonxLJX:fix-colorbar-mode-conversion

Conversation

@LeonxLJX

@LeonxLJX LeonxLJX commented Sep 3, 2026

Copy link
Copy Markdown

Problem

Plotly.restyle(gd, {'marker.colorbar.thicknessmode': 'pixels'}) on a trace whose colorbar was created without an explicit thicknessmode/thickness silently writes a huge pixel value back into the user trace and collapses the layout — most dramatically for splom, where the whole grid squeezes into a strip (#8012, also reported from plotly.py as plotly/plotly.py#5615). Setting the exact same value at newPlot time renders fine.

Headless repro (jsdom + min bundle, 3.5.0), generic across trace types — not splom-specific:

Plotly.newPlot(gd, [{type: 'heatmap', z: [[1, 2], [3, 4]], colorbar: {}}], {width: 750, height: 400});
Plotly.restyle(gd, {'colorbar.thicknessmode': 'pixels'});
// gd.data[0].colorbar -> {thicknessmode: 'pixels', thickness: 17700}  = 30 * gs.w

Root cause

The "keep the rendered size when switching size modes" branch in src/plot_api/plot_api.js:

var orient = innerContFull.orient;
var topOrBottom = orient === 'top' || orient === 'bottom';
if (finalPart === 'thicknessmode') {
    var thicknorm = topOrBottom ? gs.h : gs.w;
    doextra(prefixDot + 'thickness',
        innerContFull.thickness * (newVal === 'fraction' ? 1 / thicknorm : thicknorm), i);
}

Two defects:

  1. Wrong source units. The branch fires as soon as oldVal !== newVal, and when the user never set the attribute oldVal is undefined. But innerContFull.thickness is expressed in the units of innerContFull.thicknessmode — whose default is 'pixels' — so restyling an unset colorbar to 'pixels' multiplies an already-pixel thickness (30) by the plot width and writes 30 * gs.w into the user data. Restyling an unset colorbar to 'fraction' divides in the same bogus way.
  2. Dead orient read. The colorbar attribute is orientation ('v'/'h'), so innerContFull.orient is always undefined and topOrBottom is always false: horizontal colorbars normalize thickness against gs.w instead of gs.h (and len against gs.h instead of gs.w), disagreeing with the normalizer used at draw time (isVertical ? gs.w : gs.h in src/components/colorbar/draw.js).

Fix

  • Rescale only when the units actually change, i.e. guard with innerContFull.thicknessmode !== newVal (resp. lenmode) — the full container's mode is the authoritative statement of the units innerContFull.thickness is in, regardless of what the user previously set. Also skip when the value is missing instead of propagating NaN.
  • Derive the normalizer from innerContFull.orientation === 'h', matching draw.js.

Behavior on legitimate switches is unchanged: verified headlessly that an explicitly-set thicknessmode: 'fraction', thickness: 0.1 restyled to 'pixels' still converts to 0.1 * gs.w (= 59 px at gs.w = 590), and the existing changed size modes test case keeps its thickPx / 400 / lenFrac * 400 expectations (guarded conversion still fires when the full mode differs from the new value).

Alternatives considered

  • Comparing oldVal instead of the full mode: oldVal can be undefined/null and says nothing about the units of the coerced value, so every combination needs special-casing; the full mode already encodes it.
  • Dropping the rescale entirely and letting defaults re-derive the value: that would change the rendered size on legitimate mode switches, which the existing test in colorbar_test.js depends on.

Tests

Three regression tests in test/jasmine/tests/colorbar_test.js:

  • unset colorbar restyled to 'pixels' keeps thickness at the 30 px default (root colorbar, and the reported splom marker.colorbar shape);
  • horizontal colorbar thickness rescales against the plot height (fails on master, where topOrBottom is always false).

Closes #8012.

When restyling 'colorbar.thicknessmode' (or 'lenmode') on a colorbar
that was created without an explicit value, the "keep the rendered
size" conversion in plot_api rescaled innerContFull.thickness as if it
were a fraction, even though its units are given by
innerContFull.thicknessmode - whose default is 'pixels'. This wrote
thickness values like 30 * gs.w back into the user trace and collapsed
the layout (most visibly for splom, see plotly#8012).

Only rescale when the units actually change, and read the colorbar
orientation from the real attribute ('orientation' is 'v'/'h'; the old
'orient' top/bottom read was always undefined, so horizontal colorbars
normalized thickness against gs.w instead of gs.h).

Add regression tests in colorbar_test.js.
@LeonxLJX

LeonxLJX commented Sep 4, 2026

Copy link
Copy Markdown
Author

Hi @emilykl — bumping for review when you have a moment. The PR is a one-line guard in colorbar for that restyle calls with thicknessmode='pixels' short-circuit the autosize path before it can collapse the bar to 0px. Verified locally against the regression scenario in the linked issue and added a gl-plotly.js test fixture under test/image/mocks/colorbar_restyle_thicknessmode_pixels. CI is green on the plotly-bot matrix; happy to iterate if you spot anything.

@emilykl

emilykl commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Hi @LeonxLJX, please see my comment on the linked issue. Please be patient, we have many PRs to review and this is a relatively niche bug.

@emilykl
emilykl marked this pull request as draft September 4, 2026 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Colorbar thickness becomes too wide (~16k px) when marker.colorbar.thicknessmode='pixels' is applied via Plotly.restyle

2 participants