Re-using update function for 2 plotly-dash figures?
Question:
Context
After having created a code that adds an arbitrary number of graphs in the Dash web interface, I was trying to re-use the updater function, as it is the same for each respective graph.
Issue
When I inspect the graph, they are both the same graph (the last one that was created/updated). This is determined by inspecting the hovertext value of a particular node that is different between the two graphs.
Dash Layout Code
The dash app layout is created with:
@typechecked
def create_app_layout(
*,
app: dash.Dash,
dash_figures: Dict[str, go.Figure],
plotted_graphs: Dict[str, nx.DiGraph],
temporal_node_colours_dict: Dict[str, List],
) -> dash.Dash:
"""Creates the app layout."""
html_figures: List = []
for graph_name in plotted_graphs.keys():
# Create html figures with different id's.
html_figures.append(
dcc.Slider(
id=f"color-set-slider{graph_name}",
min=0,
max=len(temporal_node_colours_dict[graph_name][0]) - 1,
value=0,
marks={
i: str(i) for i in range(len(temporal_node_colours_dict[graph_name][0]))
},
step=None,
)
)
html_figures.append(
html.Div(dcc.Graph(id=f"Graph{graph_name}", figure=dash_figures[graph_name]))
)
print(f'graph_name={graph_name}, val={dash_figures[graph_name]}')
# Store html graphs in layout.
app.layout = html.Div(
html_figures
)
return app
Dash Update code:
The dash graphs are updated with:
@typechecked
def support_updates(
*,
app: dash.Dash,
dash_figures: Dict[str, go.Figure],
identified_annotations_dict: Dict[str, List[NamedAnnotation]],
plot_config: Plot_config,
plotted_graphs: Dict[str, nx.DiGraph],
temporal_node_colours_dict: Dict[str, List],
temporal_node_opacity_dict: Dict[str, List],
) -> None:
"""Allows for updating of the various graphs."""
# State variable to keep track of current color set
initial_t = 0
for graph_name, plotted_graph in plotted_graphs.items():
@app.callback(
Output(f"Graph{graph_name}", "figure"),
[Input(f"color-set-slider{graph_name}", "value")],
)
def update_color(
t: int,
) -> go.Figure:
# ) -> None:
"""Updates the colour of the nodes and edges based on user
input."""
if len(temporal_node_colours_dict[graph_name][0]) == 0:
raise ValueError(
"Not enough timesteps were found. probably took timestep "
+ "of ignored node."
)
update_node_colour_and_opacity(
dash_figure=dash_figures[graph_name],
identified_annotations=identified_annotations_dict[graph_name],
plot_config=plot_config,
plotted_graph=plotted_graph,
t=t,
temporal_node_colours=temporal_node_colours_dict[graph_name],
temporal_node_opacity=temporal_node_opacity_dict[graph_name],
)
update_node_colour(
dash_figure=dash_figures[graph_name],
plot_config=plot_config,
plotted_graph=plotted_graph,
t=t,
)
update_node_hovertext(
dash_figure=dash_figures[graph_name],
plot_config=plot_config,
plotted_graph=plotted_graph,
t=t,
)
return dash_figures[graph_name]
update_color(
t=initial_t,
)
Example
The first image shows the upper graph is the adapted graph (vth=4
=last row of first block):
Then, after waiting a second or 2 (while the graph is updating (automatically upon initialization), it jumps to being the 2nd graph, with vth=9999
:
Question
How can I ensure both/all graphs are unique and stay unique whilst updating them, whilst re-using the update function?
Debugging
I noticed that when I remove the return statement from the update_color(
function, the graphs remain unique. However, that is because the graph does not update anymore at all.
So I assume the object that is returned by the updater function is applied to both figures, instead of only to the figure to which it pertains according to the layout
names.
From what I understand from this documentation, I overwrite the figure data. However, if that interpretation is correct, I do not yet understand how, because I return dash_figures[graph_name]
which is a different object for the different graph names.
Bandaid Solution
When I manually copy the updater function, like:
@typechecked
def support_updates(
*,
app: dash.Dash,
dash_figures: Dict[str, go.Figure],
identified_annotations_dict: Dict[str, List[NamedAnnotation]],
plot_config: Plot_config,
plotted_graphs: Dict[str, nx.DiGraph],
temporal_node_colours_dict: Dict[str, List],
temporal_node_opacity_dict: Dict[str, List],
) -> None:
"""Allows for updating of the various graphs."""
# State variable to keep track of current color set
initial_t = 0
graph_name_one='adapted_snn_graph'
first_plotted_graph=plotted_graphs[graph_name_one]
@app.callback(
Output(f"Graph{graph_name_one}", "figure"),
[Input(f"color-set-slider{graph_name_one}", "value")],
)
def update_color_one(
t: int,
) -> go.Figure:
# ) -> None:
"""Updates the colour of the nodes and edges based on user
input."""
if len(temporal_node_colours_dict[graph_name_one][0]) == 0:
raise ValueError(
"Not enough timesteps were found. probably took timestep "
+ "of ignored node."
)
update_node_colour_and_opacity(
dash_figure=dash_figures[graph_name_one],
identified_annotations=identified_annotations_dict[graph_name_one],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_one],
t=t,
temporal_node_colours=temporal_node_colours_dict[graph_name_one],
temporal_node_opacity=temporal_node_opacity_dict[graph_name_one],
)
update_node_colour(
dash_figure=dash_figures[graph_name_one],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_one],
t=t,
)
update_node_hovertext(
dash_figure=dash_figures[graph_name_one],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_one],
t=t,
)
return dash_figures[graph_name_one]
update_color_one(
t=initial_t,
)
# Manual copy
graph_name_two='rad_adapted_snn_graph'
second_plotted_graph=plotted_graphs[graph_name_two]
@app.callback(
Output(f"Graph{graph_name_two}", "figure"),
[Input(f"color-set-slider{graph_name_two}", "value")],
)
def update_color_two(
t: int,
) -> go.Figure:
# ) -> None:
"""Updates the colour of the nodes and edges based on user
input."""
if len(temporal_node_colours_dict[graph_name_two][0]) == 0:
raise ValueError(
"Not enough timesteps were found. probably took timestep "
+ "of ignored node."
)
update_node_colour_and_opacity(
dash_figure=dash_figures[graph_name_two],
identified_annotations=identified_annotations_dict[graph_name_two],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_two],
t=t,
temporal_node_colours=temporal_node_colours_dict[graph_name_two],
temporal_node_opacity=temporal_node_opacity_dict[graph_name_two],
)
update_node_colour(
dash_figure=dash_figures[graph_name_two],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_two],
t=t,
)
update_node_hovertext(
dash_figure=dash_figures[graph_name_two],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_two],
t=t,
)
return dash_figures[graph_name_two]
update_color_two(
t=initial_t,
)
it does work (the graphs remain unique and updatable).
Answers:
It seems that the last function definition overrides the others, for this to work you would need to use distinct (dynamic) function names.
Instead of having one callback for each graph, you can define one single callback by leveraging pattern-matching callback selectors.
The pattern-matching callback selectors MATCH
, ALL
, & ALLSMALLER
allow
you to write callbacks that respond to or update an arbitrary or
dynamic number of components.
The idea is to use composite id’s (type+index) using dictionaries rather than strings, so that we can identify a given component as being the nth component of a specific type, ie.
# Layout code
html_figures: List = []
# for graph_name in plotted_graphs.keys():
for index, graph_name in enumerate(plotted_graphs.keys()):
# Use these ids
graph_id = {'type': 'graph', 'index': index}
slider_id = {'type': 'color-set-slider', 'index': index}
# ...
And in the update function (note we have to use a State()
in order to retrieve the actual id of the input component that triggers the callback) :
# Update code
# State variable to keep track of current color set
initial_t = 0
# for graph_name, plotted_graph in plotted_graphs.items():
items = list(plotted_graphs.items())
@app.callback(
Output({'type': 'graph', 'index': MATCH}, "figure"),
Input({'type': 'color-set-slider', 'index': MATCH}, "value"),
State({'type': 'color-set-slider', 'index': MATCH}, 'id'),
)
def update_color(
t: int,
id: Dict[str, Union[str, int]]
) -> go.Figure:
"""Updates the colour of the nodes and edges based on user input."""
index = id['index']
graph_name, plotted_graph = items[index]
# ...
Context
After having created a code that adds an arbitrary number of graphs in the Dash web interface, I was trying to re-use the updater function, as it is the same for each respective graph.
Issue
When I inspect the graph, they are both the same graph (the last one that was created/updated). This is determined by inspecting the hovertext value of a particular node that is different between the two graphs.
Dash Layout Code
The dash app layout is created with:
@typechecked
def create_app_layout(
*,
app: dash.Dash,
dash_figures: Dict[str, go.Figure],
plotted_graphs: Dict[str, nx.DiGraph],
temporal_node_colours_dict: Dict[str, List],
) -> dash.Dash:
"""Creates the app layout."""
html_figures: List = []
for graph_name in plotted_graphs.keys():
# Create html figures with different id's.
html_figures.append(
dcc.Slider(
id=f"color-set-slider{graph_name}",
min=0,
max=len(temporal_node_colours_dict[graph_name][0]) - 1,
value=0,
marks={
i: str(i) for i in range(len(temporal_node_colours_dict[graph_name][0]))
},
step=None,
)
)
html_figures.append(
html.Div(dcc.Graph(id=f"Graph{graph_name}", figure=dash_figures[graph_name]))
)
print(f'graph_name={graph_name}, val={dash_figures[graph_name]}')
# Store html graphs in layout.
app.layout = html.Div(
html_figures
)
return app
Dash Update code:
The dash graphs are updated with:
@typechecked
def support_updates(
*,
app: dash.Dash,
dash_figures: Dict[str, go.Figure],
identified_annotations_dict: Dict[str, List[NamedAnnotation]],
plot_config: Plot_config,
plotted_graphs: Dict[str, nx.DiGraph],
temporal_node_colours_dict: Dict[str, List],
temporal_node_opacity_dict: Dict[str, List],
) -> None:
"""Allows for updating of the various graphs."""
# State variable to keep track of current color set
initial_t = 0
for graph_name, plotted_graph in plotted_graphs.items():
@app.callback(
Output(f"Graph{graph_name}", "figure"),
[Input(f"color-set-slider{graph_name}", "value")],
)
def update_color(
t: int,
) -> go.Figure:
# ) -> None:
"""Updates the colour of the nodes and edges based on user
input."""
if len(temporal_node_colours_dict[graph_name][0]) == 0:
raise ValueError(
"Not enough timesteps were found. probably took timestep "
+ "of ignored node."
)
update_node_colour_and_opacity(
dash_figure=dash_figures[graph_name],
identified_annotations=identified_annotations_dict[graph_name],
plot_config=plot_config,
plotted_graph=plotted_graph,
t=t,
temporal_node_colours=temporal_node_colours_dict[graph_name],
temporal_node_opacity=temporal_node_opacity_dict[graph_name],
)
update_node_colour(
dash_figure=dash_figures[graph_name],
plot_config=plot_config,
plotted_graph=plotted_graph,
t=t,
)
update_node_hovertext(
dash_figure=dash_figures[graph_name],
plot_config=plot_config,
plotted_graph=plotted_graph,
t=t,
)
return dash_figures[graph_name]
update_color(
t=initial_t,
)
Example
The first image shows the upper graph is the adapted graph (vth=4
=last row of first block):
Then, after waiting a second or 2 (while the graph is updating (automatically upon initialization), it jumps to being the 2nd graph, with vth=9999
:
Question
How can I ensure both/all graphs are unique and stay unique whilst updating them, whilst re-using the update function?
Debugging
I noticed that when I remove the return statement from the update_color(
function, the graphs remain unique. However, that is because the graph does not update anymore at all.
So I assume the object that is returned by the updater function is applied to both figures, instead of only to the figure to which it pertains according to the layout
names.
From what I understand from this documentation, I overwrite the figure data. However, if that interpretation is correct, I do not yet understand how, because I return dash_figures[graph_name]
which is a different object for the different graph names.
Bandaid Solution
When I manually copy the updater function, like:
@typechecked
def support_updates(
*,
app: dash.Dash,
dash_figures: Dict[str, go.Figure],
identified_annotations_dict: Dict[str, List[NamedAnnotation]],
plot_config: Plot_config,
plotted_graphs: Dict[str, nx.DiGraph],
temporal_node_colours_dict: Dict[str, List],
temporal_node_opacity_dict: Dict[str, List],
) -> None:
"""Allows for updating of the various graphs."""
# State variable to keep track of current color set
initial_t = 0
graph_name_one='adapted_snn_graph'
first_plotted_graph=plotted_graphs[graph_name_one]
@app.callback(
Output(f"Graph{graph_name_one}", "figure"),
[Input(f"color-set-slider{graph_name_one}", "value")],
)
def update_color_one(
t: int,
) -> go.Figure:
# ) -> None:
"""Updates the colour of the nodes and edges based on user
input."""
if len(temporal_node_colours_dict[graph_name_one][0]) == 0:
raise ValueError(
"Not enough timesteps were found. probably took timestep "
+ "of ignored node."
)
update_node_colour_and_opacity(
dash_figure=dash_figures[graph_name_one],
identified_annotations=identified_annotations_dict[graph_name_one],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_one],
t=t,
temporal_node_colours=temporal_node_colours_dict[graph_name_one],
temporal_node_opacity=temporal_node_opacity_dict[graph_name_one],
)
update_node_colour(
dash_figure=dash_figures[graph_name_one],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_one],
t=t,
)
update_node_hovertext(
dash_figure=dash_figures[graph_name_one],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_one],
t=t,
)
return dash_figures[graph_name_one]
update_color_one(
t=initial_t,
)
# Manual copy
graph_name_two='rad_adapted_snn_graph'
second_plotted_graph=plotted_graphs[graph_name_two]
@app.callback(
Output(f"Graph{graph_name_two}", "figure"),
[Input(f"color-set-slider{graph_name_two}", "value")],
)
def update_color_two(
t: int,
) -> go.Figure:
# ) -> None:
"""Updates the colour of the nodes and edges based on user
input."""
if len(temporal_node_colours_dict[graph_name_two][0]) == 0:
raise ValueError(
"Not enough timesteps were found. probably took timestep "
+ "of ignored node."
)
update_node_colour_and_opacity(
dash_figure=dash_figures[graph_name_two],
identified_annotations=identified_annotations_dict[graph_name_two],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_two],
t=t,
temporal_node_colours=temporal_node_colours_dict[graph_name_two],
temporal_node_opacity=temporal_node_opacity_dict[graph_name_two],
)
update_node_colour(
dash_figure=dash_figures[graph_name_two],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_two],
t=t,
)
update_node_hovertext(
dash_figure=dash_figures[graph_name_two],
plot_config=plot_config,
plotted_graph=plotted_graphs[graph_name_two],
t=t,
)
return dash_figures[graph_name_two]
update_color_two(
t=initial_t,
)
it does work (the graphs remain unique and updatable).
It seems that the last function definition overrides the others, for this to work you would need to use distinct (dynamic) function names.
Instead of having one callback for each graph, you can define one single callback by leveraging pattern-matching callback selectors.
The pattern-matching callback selectors
MATCH
,ALL
, &ALLSMALLER
allow
you to write callbacks that respond to or update an arbitrary or
dynamic number of components.
The idea is to use composite id’s (type+index) using dictionaries rather than strings, so that we can identify a given component as being the nth component of a specific type, ie.
# Layout code
html_figures: List = []
# for graph_name in plotted_graphs.keys():
for index, graph_name in enumerate(plotted_graphs.keys()):
# Use these ids
graph_id = {'type': 'graph', 'index': index}
slider_id = {'type': 'color-set-slider', 'index': index}
# ...
And in the update function (note we have to use a State()
in order to retrieve the actual id of the input component that triggers the callback) :
# Update code
# State variable to keep track of current color set
initial_t = 0
# for graph_name, plotted_graph in plotted_graphs.items():
items = list(plotted_graphs.items())
@app.callback(
Output({'type': 'graph', 'index': MATCH}, "figure"),
Input({'type': 'color-set-slider', 'index': MATCH}, "value"),
State({'type': 'color-set-slider', 'index': MATCH}, 'id'),
)
def update_color(
t: int,
id: Dict[str, Union[str, int]]
) -> go.Figure:
"""Updates the colour of the nodes and edges based on user input."""
index = id['index']
graph_name, plotted_graph = items[index]
# ...