Search code examples
plotlyvisualizationplotly-python

Plotly update_menus hiding traces problem


Newbie in Plotly with Python and StackOverflow here. I have created a figure that includes a number of box plots, a scatter plot and a few more elements that can be relayed out.

My data have gaps that I want to optionally toggle on and off from appearing in the graph using a dropdown list (see Image 1).

Image 1

In one case, I can hide the gaps successfully but the scatter plot loses its X axis values and is graphed after the end of the labeled data (see Image 2). This is produced by this line of code: dict(label="Exclude missing data", method="update", args=[{"x":df.Run[x_some],"visible": x_some}])

Image 2

In the other case I can hide the gaps in the data, the scatter plot works but the labels of the X axis that were "hidden" move to the end of the graph (see Image 3). This is produced by this line of code: dict(label="Exclude missing data", method="update", args=[{"visible": x_some}])

Image 3

Are there any hints on what I'm doing wrong and how to fix this?

Please find some sample code below:

import pandas as pd
import numpy as np
import plotly.io as pio
import datetime as dt
import plotly.graph_objects as go

pio.renderers.default = 'browser'
pd.options.plotting.backend = "plotly"
fig = go.Figure()
N = 100
ydata = np.random.randint(600, 1200, size=(100, 5))
x = ["R"+str(i) for i in range(len(ydata))]
df = pd.DataFrame(ydata, columns=["M1", "M2", "M3", "M4", "M5"])
df["X"] = x
gaps = np.r_[20:40, 60:70]
df.iloc[gaps, 0:5] = np.nan
df["Mean"] = df.iloc[:, 0:5].mean(axis=1).round(0)
cols = df.columns.str.contains("M")

# %% figure setup
layout = dict(
    height=700, width=1500,
    xaxis_title="Run",
    xaxis=dict(autorange=False, range=[0, N], type="category"),
    yaxis=dict(autorange=True),
    font=dict(family="Courier New, monospace", size=12, color="RebeccaPurple"),
    showlegend=False
)

fig.layout = layout

mean_line = go.Scatter(x=df.X, y=df.Mean, connectgaps=True, mode='lines+markers', name='Mean line', showlegend=False)

boxes = [go.Box(name=df.X[i], y=df.iloc[i, cols], boxpoints=False, boxmean=True, notched=True, showlegend=False) for i in range(N)]

fig.add_traces(boxes)

x_all = df.index == df.index
x_some = (df.Mean > 0).values

fig.add_trace(mean_line)

fig.update_layout(updatemenus=[
    dict(type="dropdown", direction="down", buttons=list([
        dict(label="Include missing data", method="update", args=[{"visible": x_all}]),
        dict(label="Exclude missing data", method="update", args=[{"visible": x_some}])
    ]), pad={"l": 10, "t": 10}, showactive=True, x=0.23, xanchor="left", y=1.1, yanchor="top")])

z = fig.to_dict()
fig.show()

Solution

  • The reason this is occurring is because you forgot to account for the last trace which is the scatter, and it still includes x-values that are NaN. (there are 100 box traces and 1 scatter trace, so x_all and x_some should actually have length 101 – in your original code, these two boolean arrays have length 100, but plotly assumes that you mean to show the last scatter trace)

    To fix this, we can add another scatter trace with missing values dropped – we can make this not visible by default, and only show this scatter when you select the "Exclude missing data" from the dropdown.

    Note: x_all and x_some will now have length 102. x_all should look like [True,... True, True, False] because we show the first 100 boxplots, and then show the first scatter without nulls dropped, but then don't show the second scatter which has nulls dropped. And x_some should look like [True...False... False... True, False, True] because some of the box plots traces that are empty won't be shown, and then we don't show the first scatter without nulls dropped, but show the second scatter which has nulls dropped.

    import pandas as pd
    import numpy as np
    import plotly.io as pio
    import datetime as dt
    import plotly.graph_objects as go
    
    pio.renderers.default = 'browser'
    pd.options.plotting.backend = "plotly"
    fig = go.Figure()
    N = 100
    ydata = np.random.randint(600, 1200, size=(100, 5))
    x = ["R"+str(i) for i in range(len(ydata))]
    df = pd.DataFrame(ydata, columns=["M1", "M2", "M3", "M4", "M5"])
    df["X"] = x
    gaps = np.r_[20:40, 60:70]
    df.iloc[gaps, 0:5] = np.nan
    df["Mean"] = df.iloc[:, 0:5].mean(axis=1).round(0)
    cols = df.columns.str.contains("M")
    
    # figure setup
    layout = dict(
        height=700, width=1500,
        xaxis_title="Run",
        xaxis=dict(autorange=False, range=[0, N], type="category"),
        yaxis=dict(autorange=True),
        font=dict(family="Courier New, monospace", size=12, color="RebeccaPurple"),
        showlegend=False
    )
    
    fig.layout = layout
    
    mean_line = go.Scatter(x=df.X, y=df.Mean, connectgaps=True, mode='lines+markers', name='Mean line', showlegend=False)
    
    ## create mean line with excluded values missing (not visible by default)
    df_non_null = df.dropna()
    mean_line_exclude_missing = go.Scatter(x=df_non_null.X, y=df_non_null.Mean, connectgaps=False, mode='lines+markers', name='Mean line', showlegend=False, visible=False)
    
    boxes = [go.Box(name=df.X[i], y=df.iloc[i, cols], boxpoints=False, boxmean=True, notched=True, showlegend=False) for i in range(N)]
    
    fig.add_traces(boxes)
    
    x_all = df.index == df.index
    x_all = np.append(x_all, [True, False])
    
    x_some = (df.Mean > 0).values
    x_some = np.append(x_some, [False, True])
    
    ## the last two traces will be the mean line, and the mean line (with nulls excluded)
    fig.add_trace(mean_line)
    fig.add_trace(mean_line_exclude_missing)
    
    fig.update_layout(updatemenus=[
        dict(type="dropdown", direction="down", buttons=list([
            dict(label="Include missing data", method="update", args=[{"visible": x_all}]),
            dict(label="Exclude missing data", method="update", args=[{"visible": x_some}])
        ]), pad={"l": 10, "t": 10}, showactive=True, x=0.23, xanchor="left", y=1.1, yanchor="top")])
    
    z = fig.to_dict()
    fig.show()
    

    enter image description here