I need some advice concerning the updatemenu
buttons in plotly. I am dynamically changing the plot range of a graph and I trigger different traces with the updatemenu
buttons. However, as soon as I move the range slider, the default trace is shown again (and the previous button is unselected or not active anymore). I would like the view to stay on the previously selected (via updatemenu
button) trace.
I spend like 8 hours trying to fix it and this is my last resort :D Otherwise, it won't get fixed.
I do believe tho, that it is possible. I think it is just beyond my skills at the moment. Maybe someone can come up with a clever implementation.
Thanks in Advance :)
uirevision
update
method and saving it in a global variablevisible
property for any traceUpdate: Idea 4 works as intended, see answer below.
dcc.Store
it didn't work, cause the value changed without me modifying it.restyle
and relayout
method. I wasn't able to toggle the traces with those two methods, therefore I didn't continue.@ app.callback()
with dash.State
. However, I wasn't able to get the currently active button.visible
property, no trace is visible upon changes. Furthermore, I couldn't recreate the wanted behaviour.The current version of the application is way more complicated. I have many more elements in the callbacks and the dataset is a little more complicated.
Due to changing columns in the dataset, it is important, that I create the traces and the update menu buttons after reading out the columns. It would be easier with a fixed amount and fixed labels/ids.
Imports:
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go
import numpy as np
import webbrowser
import pandas as pd
Application:
def launch_app(data) -> None:
# Creates the Application
app = dash.Dash(__name__)
# HTML Layout
app.layout = html.Div(children=[
html.Div([
# Graph Container
html.Div(id='graph-container'),
# Range Slider
html.Div(
dcc.RangeSlider(id='range-slider', min=0, vertical=True),
),
], style={"display": "flex", "align-items": "center", "justify-content": "space-evenly"}),
])
# Callback to update graphs when slider changes
@ app.callback(
dash.Output("range-slider", "value"),
dash.Output("graph-container", "children"),
dash.State("graph-container", "children"),
dash.Input("range-slider", "value"),
)
def update(graph_container, slider):
# Setting max
max = len(data["data"]["petal_length"])
# First call to initialize
if slider is None:
end_value = max
slider_value = [0, end_value]
return slider_value, update_graphs(0, end_value)
# Get active Button
active_button = graph_container[0]['props']['figure']['layout']['updatemenus'][0]['active']
# Set values depending on the trigger
start_value = slider[0]
end_value = slider[1]
slider_value = slider
return slider_value, update_graphs(start_value, end_value, active_button)
# Running the app
app.run_server(debug=True, port=8050)
Generation of new graphs:
def update_graphs(start: int, end: int, active_button: int = 0) -> list[dcc.Graph]:
"""
Updates the dcc.Graph and returns the updated ones.
Parameters
----------
start : int
lower index which was selected
end : int
upper index which was selected
active_button: int
which button is active of the plotly updatemenu buttons, by default 0
Returns
-------
list[dcc.Graph]
Updated Graph
"""
fig = go.Figure()
# Read out columns automatically
all_columns = [col for col in list(data["data"].head())]
# Generate X-Axis
xvalues = np.arange(0, len(data["data"]["petal_length"]))
# CREATION OF TRACES
for ii, col in enumerate(all_columns):
if ii == active_button:
visible = True
else:
visible = False
fig.add_trace(
go.Scatter(x=xvalues[start: end],
y=data["data"][f"{col}"][start: end],
visible=visible)
)
# Generation of visible array for buttons properties
show_list = []
for ii, val in enumerate(all_columns):
# Initialization
temp = np.zeros(len(all_columns))
temp[ii] = 1
temp = np.array(temp, dtype=bool)
show_list.append(temp)
# CREATION OF BUTTONS
all_buttons = []
for ii, col in enumerate(all_columns):
all_buttons.append(dict(label=f"{col}", method="update", args=[
{"visible": show_list[ii]},
{"yaxis": {'title': f'yaxis {col}'}}
]))
# Update Menu Buttons
fig.update_layout(
updatemenus=[
dict(
type="buttons",
active=active_button,
showactive=True,
buttons=all_buttons,
x=0.0,
y=1.2,
xanchor="left",
yanchor="top",
direction="right",
)
])
return [
dcc.Graph(
id='time',
figure=fig,
)
]
Script:
if __name__ == "__main__":
# Loading sample Data
iris = pd.read_csv(
'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv')
data = {"id": 20, "data": iris}
# Opens the server
webbrowser.open("http://127.0.0.1:8050/")
# Launches the server
launch_app(data)
Your idea (4) to include your figure (or its parent object) as a state in your callback function update
should work. You only have to find the correct "active" property and then pass on the active button as an argument to your update_graphs
function.
The following changes should do the job:
@ app.callback(
dash.Output("range-slider", "value"),
dash.Output("graph-container", "children"),
dash.Input("graph-container", "children"),
dash.Input("range-slider", "value"),
)
def update(graph_container, slider):
update_graphs
.active_button = graph_container[1]['props']['figure']['layout']['updatemenus'][0]['active']
return slider_value, update_graphs(start_value, end_value, active_button=active_button)
update_graphs
and use it.def update_graphs(start: int, end: int, active_button: int):
# Some stuff
# Update Menu Buttons
fig.update_layout(
updatemenus=[
dict(
# other stuff
active=active_button,
)
])