facet_row_spacing modified in a callback for px.imshow changes the figure dict but not the plot
Description of the issue
- I have an app with a callback that uses a dcc.Input (type='number') as Input and recreates the figure property of two dcc.Graphs using the input value as the
facet_row_spacingargument. - Both graphs use
facet_col,facet_col_wrapandfacet_row_spacing. - The recreation works fine for the facetted scatterplot but not for the facetted heatmap/imshow; that is, the
facet_row_spacinggets adjusted for the scatterplot but always remains the same (the initial value) for the imshow. - The figure dict gets updated correctly in both cases, as shown in the div under each figure - I know this because the difference between the domain end of the first row and the domain start of the second row is equal to the value specified in the dcc.Input.
- The subplot titles also get moved to their correct (new) position.
- However, if we use the plotly crowbar instead to retrieve the figure dict, the values for yaxis domains show a value that matches the spacing we see on the screen, that is: always the initial facet_row_spacing value for the facetted imshow [this is not shown in the video].
JS code to copy in the console to get the plotly crowbar:
javascript:document.querySelectorAll(".js-plotly-plot").forEach(function(gd){ const txt = document.createElement("textarea"); txt.appendChild(document.createTextNode(JSON.stringify({data: gd.data, layout: gd.layout})));txt.style.position='absolute';txt.style.zIndex=9999; gd.appendChild(txt);})
https://github.com/user-attachments/assets/e6684d8c-9818-4631-9e7b-0c51bfbcd94e
Things I have not tested
- If it happens with
facet_row+facet_col_spacingtoo.
MRE
Python 3.10 plotly==5.24.1 dash==2.18.1 plotlyjs==2.35.2 (according to the modebar)
from dash import Dash, html, dcc, callback, Input, Output
import plotly
import json
# https://community.plotly.com/t/facet-row-and-facet-row-labels-when-using-imshow/85905
print(plotly.__version__)
app = Dash(__name__)
import numpy as np
import plotly.express as px
def generate_fig1(row_spacing):
fig1 = px.imshow(
np.random.uniform(0, 1, size = (18, 28, 28)),
facet_col = 0, facet_col_wrap = 6, facet_row_spacing=row_spacing,
)
return fig1
df=px.data.gapminder().query("continent == 'Americas'")
def generate_fig2(row_spacing):
fig2 = px.line(
df, x="year", y="lifeExp",
facet_col="country", facet_col_wrap=8, facet_row_spacing=row_spacing,
)
return fig2
app.layout=html.Div([
dcc.Input(value=0.3, min=0, max=0.3, step=0.05, id='spacing', type='number'),
dcc.Graph(id='fig1'),
html.H3('fig1'),
html.Div(id='div1'),
dcc.Graph(id='fig2'),
html.H3('fig2'),
html.Div(id='div2'),
])
@callback(
Output('fig1', 'figure'),
Output('div1', 'children'),
Output('fig2', 'figure'),
Output('div2', 'children'),
Input('spacing', 'value'),
)
def update_outputs(spacing):
fig1 = generate_fig1(spacing)
fig2 = generate_fig2(spacing)
text1 = {}
for i in range (1,19):
if i == 1:
i = ""
text1[f'yaxis{i}']= fig1.layout[f'yaxis{i}']['domain']
text2 = {}
for i in range (1,26):
if i == 1:
i = ""
text2[f'yaxis{i}']= fig2.layout[f'yaxis{i}']['domain']
return fig1, str(text1), fig2, str(text2)
if __name__ == "__main__":
app.run_server(debug=True)
@michaelbabyn noticed this:
it looks like the heatmaps are getting loaded as images from a cache rather than being regenerated each time the callback updates the figure so that explains why the subplot titles are moving but the plots are staying the same size
@michaelbabyn noticed this:
it looks like the heatmaps are getting loaded as images from a cache rather than being regenerated each time the callback updates the figure so that explains why the subplot titles are moving but the plots are staying the same size
I will also note that this still showing up when my cache is disabled.
Yeah, those images also show up in a plotly.js only example (https://codepen.io/michaelbabyn/pen/OJKbBXo) which works properly so I think I was too quick to blame the issue on caching
Note: this is also happening with facet_col_spacing.
I wonder if this is related to https://github.com/plotly/dash/issues/2405
Another data point for debugging: when scaleanchor: 'y' is removed from layout['xaxis'] in px.imshow here, this is the result:
We do want the image to be scaled, but this appears to be an issue with setting that layout config w/ facet plots. Potentially an issue in plotly.js but I'm still investigating.
Looks like this line in plotly.js is where the domain is set incorrectly after the first render. @alexcjohnson wrote up some of the reasoning behind these lines in this PR comment but I'm still trying to wrap my head around the solution here
Note: this change fixes the issue in this particular case. Not sure if it causes issues elsewhere:
--- a/src/plots/cartesian/constraints.js
+++ b/src/plots/cartesian/constraints.js
@@ -473,8 +473,7 @@ exports.enforce = function enforce(gd) {
axisID = axisIDs[j];
axes[axisID] = ax = fullLayout[id2name(axisID)];
- if(ax._inputDomain) ax.domain = ax._inputDomain.slice();
- else ax._inputDomain = ax.domain.slice();
+ ax._inputDomain = ax.domain.slice();
if(!ax._inputRange) ax._inputRange = ax.range.slice();
@alexcjohnson I'm wondering perhaps in relayout we need to keep track of initial domains being changed similar to what is done for axis ranges here? https://github.com/plotly/plotly.js/blob/0d322117a58c2ca3e098ca18cd0a9937ed74ebaa/src/plot_api/plot_api.js#L1926-L1929
I’m not sure what the right solution will be, @archmoj may be on the right track or maybe something simpler like https://github.com/plotly/plotly.js/pull/7274 will work. As I said in a DM to @marthacryan, in addition to fixing the bug here:
whatever you do, I think the key test will be something like: create a plot that’s wider than it is tall, with a constraint that makes the x axis domain shrink. Then either increase its height or decrease its width, the x axis domain should increase beyond where it ended up initially, but not beyond the originally specified domain.
And to be clear, that height or width change should be via relayout or some such, ie not providing the whole figure again with the original domains.
Interesting update on the debugging here: we've discovered that this bug does not occur when layout.template is not defined in the initial call to Plotly.newPlot. Even defining it as an empty dictionary appears to make the problem appear again. In this codepen, you can see that by uncommenting line 31, you can make this bug appear or not appear.