Skip to content

Reset axes button in dcc.Graph modebar doesn't apply the range specified in yaxis_range #3045

@celia-lm

Description

@celia-lm

If the figure is included from the start in a dcc.Graph, clicking Reset axes 🏠 in the modebar triggers this:

{'xaxis.autorange': True, 'xaxis.showspikes': False, 'yaxis.range': 0, 0.1, 'yaxis.showspikes': False}

However, if the dcc.Graph figure property is populated in a callback, Reset Axes 🏠 triggers this (the same effect as clicking Autoscale):

{'xaxis.autorange': True, 'xaxis.showspikes': False, 'yaxis.autorange': True, 'yaxis.showspikes': False} 

even if the figure has a specific yaxis_range=[0,0.1] and we have set yaxis_autorange=False

I believe this is because the modebar is created when the dcc.Graph component is initialized and it’s not updated if the figure changes.

Additional information:

Workaround

Create an empty figure with the desired/eventual layout, for example:

empty_fig = go.Figure(layout=go.Layout(yaxis_range=[0, 0.1])) 

and assign this as the initial value for the figure that you are going to later update with a callback:

dcc.Graph(id='fig2', figure=empty_fig)

Code to replicate the issue

Env
Python 3.10
dash==2.18.1
plotly==5.24.1

from dash import Dash, html, dcc, callback, Input, Output
import pandas as pd
import plotly.graph_objects as go
import random
import plotly
import dash

print(f"Plotly version is {plotly.__version__} and dash version is {dash.__version__}")

def generate_figs():

    N = 50

    well_df = pd.DataFrame(
        {
            "EventTime": [i for i in range(N)],
            "cmax": [random.randint(0, 100) for i in range(N)],
        }
    )

    well_names = [f"Well{i}" for i in range(N)]

    threshold = 1000
    max_y = 0.07

    fig = go.Figure()

    for well_name in well_names:

        fig.add_trace(
            go.Scatter(
                x=well_df["EventTime"],
                y=well_df["cmax"] / threshold,
                name=well_name,
                mode="lines",
            )
        )

    fig.update_layout(
        title="Combined anomaly detection",
        xaxis=dict(title=dict(text="Time")),
        yaxis=dict(range=[0, 0.1], showticklabels=True, autorange=False),
        plot_bgcolor="#ebfaeb",
        legend=dict(y=1.08, x=0.97),
    )

    # create a copy of the figure
    fig2 = go.Figure(fig)

    # your figure
    fig.add_hrect(y0=1.0, y1=max_y, line_width=0, fillcolor="red", opacity=0.2)
    # fig2
    fig2.update_layout(plot_bgcolor="rgba(255,0,0,0.2)")  # red with alpha 0.2
    fig2.add_hrect(y0=0, y1=max_y, line_width=0, fillcolor="#ebfaeb", layer="below")
    return fig, fig2

fig, fig2 = generate_figs()

# WORKAROUND
empty_fig = go.Figure(layout=go.Layout(yaxis_range=[0, 0.1]))

@callback(Output("fig2", "figure"), Input("btn", "n_clicks"), prevent_initial_call=True)
def show_fig(n):
    return fig2

app = Dash(__name__)

app.layout = html.Div(
    [
        html.Button("Click me", id="btn"),
        dcc.Graph(id="fig2"),  # add figure=empty_fig for workaround
        dcc.Markdown(id="out2"),
        dcc.Markdown(id="out2b"),
        dcc.Graph(id="fig2b", figure=fig2),
    ]
)

# callbacks to check the effects on the figure
@callback(
    Output("out2", "children"), Input("fig2", "relayoutData"), prevent_initial_call=True
)
def showchanges2(rl):
    return f"**Relayout:**{rl}"

@callback(
    Output("out2b", "children"),
    Input("fig2b", "relayoutData"),
    prevent_initial_call=True,
)
def showchanges2b(rl):
    return f"**Relayout:**{rl}"

if __name__ == "__main__":
    app.run_server(debug=True)

Metadata

Metadata

Assignees

Labels

P3backlogsev-3annoyance with workaround

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions