Skip to content

facet_row_spacing modified in a callback for px.imshow changes the figure dict but not the plot #4794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
celia-lm opened this issue Oct 11, 2024 · 11 comments
Assignees
Labels
bug something broken P2 considered for next cycle sev-3 annoyance with workaround

Comments

@celia-lm
Copy link

celia-lm commented Oct 11, 2024

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_spacing argument.
  • Both graphs use facet_col, facet_col_wrap and facet_row_spacing.
  • The recreation works fine for the facetted scatterplot but not for the facetted heatmap/imshow; that is, the facet_row_spacing gets 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);})
Screen.Recording.2024-10-11.at.13.56.32.mov

Things I have not tested

  • If it happens with facet_row+facet_col_spacing too.

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)
@celia-lm celia-lm added the bug something broken label Oct 11, 2024
@celia-lm
Copy link
Author

@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
image

@gvwilson gvwilson self-assigned this Oct 11, 2024
@gvwilson gvwilson changed the title [BUG] facet_row_spacing modified in a callback for px.imshow changes the figure dict but not the plot facet_row_spacing modified in a callback for px.imshow changes the figure dict but not the plot Oct 11, 2024
@gvwilson gvwilson added sev-3 annoyance with workaround P2 considered for next cycle labels Oct 11, 2024
@gvwilson gvwilson assigned emilykl and unassigned gvwilson Oct 15, 2024
@gvwilson gvwilson assigned marthacryan and unassigned emilykl Oct 31, 2024
@marthacryan
Copy link
Collaborator

@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
image

I will also note that this still showing up when my cache is disabled.

@michaelbabyn
Copy link
Contributor

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

@marthacryan
Copy link
Collaborator

Note: this is also happening with facet_col_spacing.

@marthacryan
Copy link
Collaborator

I wonder if this is related to plotly/dash#2405

@marthacryan
Copy link
Collaborator

Another data point for debugging: when scaleanchor: 'y' is removed from layout['xaxis'] in px.imshow here, this is the result:
Image
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.

@marthacryan
Copy link
Collaborator

marthacryan commented Nov 19, 2024

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

@marthacryan
Copy link
Collaborator

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();

@archmoj
Copy link
Contributor

archmoj commented Nov 19, 2024

@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

@alexcjohnson
Copy link
Collaborator

I’m not sure what the right solution will be, @archmoj may be on the right track or maybe something simpler like plotly/plotly.js#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.

@marthacryan
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken P2 considered for next cycle sev-3 annoyance with workaround
Projects
None yet
7 participants