[BUG]: marker.colorbar.thicknessmode='pixels' collapses scatter-matrix layout after Plotly.restyle, while initial render is normal · Issue #5615 · plotly/plotly.py · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG]: marker.colorbar.thicknessmode='pixels' collapses scatter-matrix layout after Plotly.restyle, while initial render is normal #5615
When initializing a Plotly scatter-matrix figure with a splom trace whose marker
colorbar has marker.colorbar.thicknessmode='pixels', the initial render displays
normally: the scatter-matrix panels remain visible and the colorbar stays narrow.
However, if the same base figure is rendered first and the same property value is
then applied dynamically through Plotly.restyle, the layout collapses. The
scatter-matrix panels are squeezed into a very narrow strip on the left, while the
colorbar gradient occupies most of the figure canvas.
The setting is accepted by plotly.py and emitted as a valid trace property, so this
appears to be a Plotly.restyle/update-time layout calculation issue for splom
marker colorbars rather than an invalid Python-side property assignment.
Screenshots/Video
Steps to reproduce
Install the screenshot dependencies if needed: pip install plotly pandas playwright && playwright install chromium.
Run this script. It saves two screenshots in the current directory:
plot1.png: Python-side initialization sets marker.colorbar.thicknessmode='pixels' before the first render. This renders
normally.
plot2.png: the same base figure renders first, then JavaScript applies the
same value with Plotly.restyle. This shows the collapsed layout.
Compare the two generated screenshots.
importplotlyimportplotly.expressaspximportplotly.ioaspioimporttimefromplaywright.sync_apiimportsync_playwrightprint(f"Plotly version: {plotly.__version__}")
defmake_fig():
df=px.data.iris()
fig=px.scatter_matrix(
df,
dimensions=["sepal_length", "sepal_width", "petal_length", "petal_width"],
color="species",
)
fig.update_layout(
xaxis={"matches": "y"},
xaxis2={"matches": "y2"},
xaxis3={"matches": "y3"},
xaxis4={"matches": "y4"},
height=900,
width=750,
dragmode="select",
selections=[
dict(x0=3, x1=4, xref="x2", y0=8, y1=6, yref="y"),
dict(x0=5, x1=1, xref="x3", y0=5, y1=4, yref="y"),
],
)
returnfigfig=make_fig()
# Screenshot 1 / plot1.png: set the value in Python before the first render.fig.data[1].marker.colorbar.thicknessmode="pixels"assertfig.data[1].type=="splom"assertfig.data[1].marker.colorbar.thicknessmode=="pixels"print(fig.data[1].marker.colorbar.to_plotly_json())
html1=pio.to_html(fig, full_html=True, include_plotlyjs="cdn")
# Screenshot 2 / plot2.png: render first, then apply the same value with JS.fig.data[1].marker.colorbar.thicknessmode=Nonehtml2=pio.to_html(fig, full_html=True, include_plotlyjs="cdn")
restyle_script="""<script>window.addEventListener("DOMContentLoaded", () => { var plotEl = document.getElementsByClassName("plotly-graph-div")[0]; setTimeout(function() { Plotly.restyle(plotEl, {"marker.colorbar.thicknessmode": "pixels"}, [1]); }, 1000);});</script>"""html2=html2.replace("</body>", restyle_script+"\n</body>")
withsync_playwright() asp:
browser=p.chromium.launch(
headless=True,
args=["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
)
page=browser.new_page(viewport={"width": 1200, "height": 800})
page.set_content(html1)
time.sleep(1.5)
page.screenshot(path="plot1.png")
page.set_content(html2)
time.sleep(2.0)
page.screenshot(path="plot2.png")
browser.close()
print("Saved plot1.png and plot2.png")
Notes
Add info here that doesn't fit in the other sections.
We think this is likely an issue in the way we're applying the restyle in this case. We may not be able to get to this particular case immediately, but would encourage community PRs!
For this and similar issues, we're not likely to prioritize a fix if the issue can only be reproduced via a combination of Python and JavaScript, since that's a fairly niche use case.
Are you able to reproduce the issue using either pure Python (via plotly_restyle()) or pure JavaScript using plotly.js?
Hi @LeonxLJX, you're welcome to look into it, but we're not likely to merge a fix unless you can demonstrate that the bug can be reproduced using only Python code (without injecting JavaScript).
Hi @emilykl, thanks for the note! I'd like to work on this issue. I'll make sure to include a clear reproduction case demonstrating the bug and verify the fix works with the scatter-matrix layout. Could you please assign this to me? Thanks!
Hi @emilykl — I ran the pure-Python reproduction you asked for. I can confirm the bug, and I think I've localized it.
Reproduction (no JavaScript injected — only the Python API):
Path A — set marker.colorbar.thicknessmode='pixels' at construction time → renders normally.
Path B — build the base splom figure, then apply the same value through fig.plotly_restyle({{'marker.colorbar.thicknessmode': 'pixels', 'marker.colorbar.thickness': 20}}, 0) → the panels collapse into a narrow strip.
The useful part: I then serialized the figure on both paths and diffed them. The trace data and the layout come out identical — both emit the same colorbar payload (thickness: 20, thicknessmode: 'pixels', same title, same showscale).
So nothing is lost or mis-serialized on the Python side: plotly.py produces exactly the same payload whether the property is set at construction or via restyle. The divergence has to happen downstream, in plotly.js's restyle-time layout pass — where the splom panel geometry appears to be recomputed against the new colorbar thickness instead of preserving the geometry from the initial layout.
My conclusion: there is no correct fix to make in this repo. Any change here would be papering over a layout calculation that lives in plotly.js, and would leave the bug fully reproducible for any other client emitting the same payload.
I'd rather not open a PR that can't fix the root cause. Instead, tell me which you'd prefer:
I open a corresponding issue on plotly-js with this reproduction attached and link the two, or
I dig into the js layout code and try to fix it there.
Happy either way — I'll hold off here until you point me at one.
@LeonxLJX Thanks for the update. If you think there's a bug in plotly.js, then yes please feel free to open an issue there, including a full reproducible example in JS.
Summary of what I found (all from the JS side, no Python involved):
The collapse is not specific to plotly_restyle/Python. In plotly.js: if a splom
trace's marker.colorbar has no explicit thickness (plotly defaults it), then applying {'marker.colorbar.thicknessmode': 'pixels'} via Plotly.restyle makes plotly.js write thickness: 16290 back into the user trace and the splom panels collapse (~125px -> 15px).
Reproduced on plotly.js 2.35.2 / 3.5.0 / 3.5.1 / 3.8.2 — so it is a plotly.js
default-coercion bug (colorbar.thickness is not re-defaulted when thicknessmode flips
to pixels), not something plotly.py can fix on its side.
Workaround confirmed: pass an explicit thickness in the same restyle.
So for this issue (#5615): the original report is valid, but the fix belongs in plotly.js.
I'd suggest keeping this issue open as a cross-reference to plotly.js#8012 (or closing it in
favor of the JS issue, since that's where the reproduction lives now). Thanks for pointing
me to the right place to chase it down!
What happens: Plotly.restyle treats changing marker.colorbar.thicknessmode as a size-mode switch and rescales thickness so the rendered size stays constant — but it assumes the current value is in the old mode's units. Since the colorbar was created with marker.colorbar = {} (no explicit thicknessmode), the full-trace value is the default 30 in pixels, and the rescale multiplies it by the plot width as if it were a fraction — writing 30 * gs.w (a five-digit number) back into the trace. The 30 k-px colorbar then squeezes the splom grid into a strip.
The plotly.js PR guards the rescale so it only fires when the units actually change; with it, your Path B renders identically to Path A. Once it's merged and released, the bundled plotly.js in plotly.py will pick it up — nothing to change on the Python side.
Activity
robertclaus commentedon Jun 17, 2026
We think this is likely an issue in the way we're applying the restyle in this case. We may not be able to get to this particular case immediately, but would encourage community PRs!
emilykl commentedon Jul 30, 2026
Thanks for the issue, @ym-xie!
For this and similar issues, we're not likely to prioritize a fix if the issue can only be reproduced via a combination of Python and JavaScript, since that's a fairly niche use case.
Are you able to reproduce the issue using either pure Python (via
plotly_restyle()) or pure JavaScript using plotly.js?LeonxLJX commentedon Aug 28, 2026
Hi! I'd like to work on this issue. Please let me know if I can be assigned. Thanks!
emilykl commentedon Aug 28, 2026
Hi @LeonxLJX, you're welcome to look into it, but we're not likely to merge a fix unless you can demonstrate that the bug can be reproduced using only Python code (without injecting JavaScript).
LeonxLJX commentedon Aug 29, 2026
Hi @emilykl, thanks for the note! I'd like to work on this issue. I'll make sure to include a clear reproduction case demonstrating the bug and verify the fix works with the scatter-matrix layout. Could you please assign this to me? Thanks!
emilykl commentedon Aug 31, 2026
@LeonxLJX Sure, go for it.
LeonxLJX commentedon Sep 2, 2026
Hi @emilykl — I ran the pure-Python reproduction you asked for. I can confirm the bug, and I think I've localized it.
Reproduction (no JavaScript injected — only the Python API):
marker.colorbar.thicknessmode='pixels'at construction time → renders normally.splomfigure, then apply the same value throughfig.plotly_restyle({{'marker.colorbar.thicknessmode': 'pixels', 'marker.colorbar.thickness': 20}}, 0)→ the panels collapse into a narrow strip.The useful part: I then serialized the figure on both paths and diffed them. The trace data and the layout come out identical — both emit the same
colorbarpayload (thickness: 20, thicknessmode: 'pixels', same title, sameshowscale).So nothing is lost or mis-serialized on the Python side:
plotly.pyproduces exactly the same payload whether the property is set at construction or via restyle. The divergence has to happen downstream, in plotly.js's restyle-time layout pass — where the splom panel geometry appears to be recomputed against the new colorbar thickness instead of preserving the geometry from the initial layout.My conclusion: there is no correct fix to make in this repo. Any change here would be papering over a layout calculation that lives in plotly.js, and would leave the bug fully reproducible for any other client emitting the same payload.
I'd rather not open a PR that can't fix the root cause. Instead, tell me which you'd prefer:
plotly-jswith this reproduction attached and link the two, orHappy either way — I'll hold off here until you point me at one.
emilykl commentedon Sep 2, 2026
@LeonxLJX Thanks for the update. If you think there's a bug in plotly.js, then yes please feel free to open an issue there, including a full reproducible example in JS.
LeonxLJX commentedon Sep 3, 2026
Hi @emilykl — following your suggestion, I reproduced this in pure JavaScript
against plotly.js itself and opened an issue there:
plotly/plotly.js#8012 — plotly/plotly.js#8012
Summary of what I found (all from the JS side, no Python involved):
plotly_restyle/Python. In plotly.js: if asplomtrace's
marker.colorbarhas no explicitthickness(plotly defaults it), then applying{'marker.colorbar.thicknessmode': 'pixels'}viaPlotly.restylemakes plotly.js writethickness: 16290back into the user trace and the splom panels collapse (~125px -> 15px).default-coercion bug (
colorbar.thicknessis not re-defaulted whenthicknessmodeflipsto
pixels), not something plotly.py can fix on its side.thicknessin the same restyle.So for this issue (#5615): the original report is valid, but the fix belongs in plotly.js.
I'd suggest keeping this issue open as a cross-reference to plotly.js#8012 (or closing it in
favor of the JS issue, since that's where the reproduction lives now). Thanks for pointing
me to the right place to chase it down!
LeonxLJX commentedon Sep 3, 2026
Root-caused this down to plotly.js and opened a fix there: plotly/plotly.js#8015 (tracking issue: plotly/plotly.js#8012).
What happens:
Plotly.restyletreats changingmarker.colorbar.thicknessmodeas a size-mode switch and rescalesthicknessso the rendered size stays constant — but it assumes the current value is in the old mode's units. Since the colorbar was created withmarker.colorbar = {}(no explicitthicknessmode), the full-trace value is the default30in pixels, and the rescale multiplies it by the plot width as if it were a fraction — writing30 * gs.w(a five-digit number) back into the trace. The 30 k-px colorbar then squeezes the splom grid into a strip.The plotly.js PR guards the rescale so it only fires when the units actually change; with it, your Path B renders identically to Path A. Once it's merged and released, the bundled plotly.js in plotly.py will pick it up — nothing to change on the Python side.