'Keep plotly updatemenu button active when changing plot range dynamically in Dash
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 :)
My Ideas
- Property
uirevision
- Custom buttons, which modify a global variable
- Creating an extra callback to listen to the
update
method and saving it in a global variable - Reading out the state of the figure before generating a new one in my callback
- Not setting the
visible
property for any trace
Update: Idea 4 works as intended, see answer below.
Some Comments to the Ideas:
- I thought this might be the easy way. However, when I set it to a constant value, it only saved the zoom/selected data. It didn't keep the previously activated trace visible.
- By far the most time consuming try due to the variable amount of buttons. I was able to read out which button was pressed, but I couldn't save it in a global variable (I know that it is not recommended, but it doesn't matter here). Even with
dcc.Store
it didn't work, cause the value changed without me modifying it. - Somehow it is only possible to listen to the
restyle
andrelayout
method. I wasn't able to toggle the traces with those two methods, therefore I didn't continue. - I tried including the figure as a state in the
@ app.callback()
withdash.State
. However, I wasn't able to get the currently active button. - The way I did it at the moment is, that it is working when initialized and one immediately sees a graph when changing the slider. If one doesn't provide the
visible
property, no trace is visible upon changes. Furthermore, I couldn't recreate the wanted behaviour.
Minimal Working Example (with answer implemented)
Comments on Implementation
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)
Solution 1:[1]
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:
- Add "graph-container" as input.
@ 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):
- Get the current active button's index and pass it to
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)
- Receive active_button index in
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,
)
])
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Gabriel309 |