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):
enter image description here
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:
enter image description here

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).

Asked By: a.t.

||

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]

    # ...
Answered By: EricLavault
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.