Rebuild page on multipage app from dcc.store?

Hello,

I have created this app
https://binvestigate-fold-analyzer2.herokuapp.com/

Where someone may indicate choices based on the compound tab. Behind the scenes, I store the names of the nodes into multiple lists with dcc.store.

There are many, many examples of using store in trivial cases like another page accessing a selection.

I cannot find anything that combines dynamic callbacks, a custom selection scheme, and mutltipage.

What should my approach be to rebuild this page when someone changes pages and then changes back?

I have tried

-modifying the distinct callbacks with hacky “callback_context.triggered” schemes. this doesnt work because they are somewhat chained, and the original callback initiator is lost
-having an entirely separate page build attempt with “page url” as input, but there can only be one output per “layout/feature pair”
-other things that i am too tired to recall

Hi there,

It might be a bit difficult to help without a small reprex of your app.

One potential problem is to define the store components in one of the pages instead of your “global” layout (where dcc.Location is).

Hi. I appreciate your reply.

I made a similar question on stackoverflow

When I get the chance, I will povide the required code. Specifically, the dcc.store is in the correct location. In general, the problem is how to program callbacks to build the layout based on “dcc.store” or "on page input (like buttons or cyto) depending on whether or not the user has already interacted with the page.

Best,
Rictuar

1 Like

Great! From your SO post, I believe that your problem has to do on how to send the node selection to the Cytoscape component, and not related to dcc.Store.

I am not familiar with this particular library, so I hope someone else can give you a better answer… But looking at Cytoscape reference, it seems to me that the selections are “read-only” and can’t be used as prop in an Output.

Index.py

from dash import dcc
from dash import html
from dash.dependencies import Input, Output

import dash_bootstrap_components as dbc
from dash import callback_context

# Connect to main app.py file
from app import app
#from app import server
from apps import cyto_compound
from apps import backend_dataset
from apps import additional_filters



app.layout = html.Div(
    [
        #storage_type='session',
        dcc.Store(id='store_cyto_compound'),
        
        dbc.Row(
            #for the moment, we put all in one column
            #but maybe later put in separate columns
            #just put one of each link into a different column
            dbc.Col(
                html.Div(
                    children=[
                        dcc.Location(id='url',pathname='',refresh=False),
                        dcc.Link('Compounds',href='/apps/cyto_compound'),
                        dcc.Link('Backend Dataset',href='/apps/backend_dataset'),
                        dcc.Link('Additional Filters',href='/apps/additional_filters')
                    ]
                ),

            )
        ),
        dbc.Row(
            dbc.Col(
                html.Div(
                    id='page_content',
                    children=[]
                )
            )
        )
    ]
)

@app.callback(
    [Output(component_id='page_content',component_property='children')],
    [Input(component_id='url',component_property='pathname')]
)
def display_page(temp_pathname):
    if temp_pathname == '/apps/cyto_compound':
        return [cyto_compound.layout]
    elif temp_pathname == '/apps/backend_dataset':
        return [backend_dataset.layout]
    elif temp_pathname == '/apps/additional_filters':
        return [additional_filters.layout]


    else:
        return 'under construction'


if __name__ == '__main__':
    app.run_server(debug=True)

cyto_compound.py layout


layout=html.Div(
    children=[
        dbc.Row(
            dbc.Col(
                children=[
                    html.Button('add compound cyto', id='button_add_cyto_compound', n_clicks=0),
                ],
                width='auto',
                align='center'
            )
        ),
        html.Div(
            id='div_cytoscape_compound_cyto',
            children=[]
        ),
        html.Div(    
            children=[
                dbc.Row(
                    dbc.Col(
                        children=[
                            html.Button('bs button', id='bs button', n_clicks=0),
                        ],
                        width='auto',
                        align='center'
                    )
                ),
            ]
        ),
    ]
)

Callbacks

@app.callback(
    [Output(component_id='div_cytoscape_compound_cyto',component_property='children')],
    #gets n_clicks=0 when app loads, thats why you get a cyto right off the bat
    [Input(component_id='button_add_cyto_compound',component_property='n_clicks')],
    [State(component_id='div_cytoscape_compound_cyto',component_property='children'),
    State(component_id='store_cyto_compound',component_property='data')],prevent_initial_callback=True
)
def add_cyto_compound(temp_n_clicks,temp_children,temp_store):

    if (callback_context.triggered[0]['prop_id']=='.'):
        for i,element in enumerate(temp_store):
            new_graph=dbc.Row(
                dbc.Col(
                    dbc.Card(
                        children=[
                            #compounds
                            cyto.Cytoscape(
                                id={
                                    'type':'cytoscape_compound',
                                    'key':i
                                },
                                layout={'name':'dagre'},
                                elements=compound_network_dict['elements'],
                                stylesheet=stylesheet,
                                minZoom=0.3,
                                maxZoom=5
                            )
                        ]
                    ),
                    width='auto',
                    align='center'
                )
            )

            temp_children.append(new_graph)


    #if (callback_context.triggered[0]['prop_id']=='.'):

    elif (callback_context.triggered[0]['prop_id']=='button_add_cyto_compound.n_clicks'):
        temp_children.append(new_graph)

    return [temp_children]

@app.callback(
    [Output(component_id={'type':'cytoscape_compound','key':MATCH},component_property='elements')],
    [Input(component_id={'type':'cytoscape_compound','key':MATCH},component_property='tapNodeData')],
    #Input(component_id='button_add_cyto_compound',component_property='n_clicks')],
    #Input(component_id='Compounds',component_property='href')],
    [State(component_id={'type':'cytoscape_compound','key':MATCH},component_property='elements'),
    State(component_id='store_cyto_compound',component_property='data')]#,prevent_initial_call=True
)
def update_node_selection(temp_tap,temp_elements,temp_store):

    if temp_tap is None:
        raise PreventUpdate

    elif callback_context.triggered[0]['prop_id']=='.':
        raise PreventUpdate

    try:
        child_nodes_and_self=nx.algorithms.dag.descendants(networkx,temp_tap['id'])
    except nx.NetworkXError:
        child_nodes_and_self=set()

    child_nodes_and_self.add(temp_tap['id'])

    child_nodes_and_self=set(map(str,child_nodes_and_self))

    for temp_node in temp_elements['nodes']:

        if temp_node['data']['id'] in child_nodes_and_self:


            if temp_node['classes']=='selected':
                temp_node['classes']='not_selected'
            elif temp_node['classes']=='not_selected':
                temp_node['classes']='selected'

    return [temp_elements]


def check_if_selected(temp_dict):

    if temp_dict['classes']=='selected':
        return str(temp_dict['data']['id'])


@app.callback(
    [Output(component_id='store_cyto_compound',component_property='data')],
    [Input(component_id={'type':'cytoscape_compound','key':ALL},component_property='elements')],
    [State(component_id='store_cyto_compound',component_property='data')]
    ,prevent_initial_call=True
)
def add_selections_to_store(temp_elements,temp_store):

    

    print('\nadd_selections_to_store')
    print(callback_context.triggered[0]['prop_id'])
    #print(temp_elements)

    if callback_context.triggered[0]['prop_id']=='.':
        raise PreventUpdate

    selected_ids_list=[list(map(check_if_selected,temp_cyto_dict['nodes'])) for temp_cyto_dict in temp_elements]

    
    return [selected_ids_list]

I understand your comment about unfamiliarity with cyto, but I believe that my question could be applied to other components that lack persist as well.

At the moment, the basic callback structure is

Button click → Adds cyto

Cyto node click → update elements

updated elements → add information to store (right now as a list)

This works such that I can filter my dataset which is on another page. But, when people return to the page with the cytos, the information is lost.

I can’t figure out how to simple incorporate "if “Compounds” link is clicked, rebuild from store.

My first thought is that I could use link/page-content from app.py as input to each of these three callbacks, then have an if statement within them that says "if the callback was link/page-content, rebuild from store, but, the callback_context.triggered becomes ‘.’ for those later in the chain, so it is impossible to tell whether or not the link click triggered the callback.

I hope that this is clear.

I see… I was wrong regarding the selection, it looks possible to pass it via theelement props.

I won’t have time to look into the details right now, but maybe I can later today or tomorrow. Maybe I will be able to help, it looks like an interesting app pattern (and yes, it does look like the problem is not cyto only).

Ok, I haven’t tested this approach, but it could be an alternative…

You could pass the state as State to the callback where you route the different pages, then you can rewrite the cyto_component layout as a function that depends on the state…
Your function should use the state to recreate the cyto components and you can use a listcomp to do the trick (just like what you are doing in the add component callback).

Then in your route conditional, you can return the invoked function with the state for cyto_component. This should in principle address your issue, because on its current form, the router callback calls the cyto_component layout with an empty list regardless of your state. I think this would also save the need to deal with the context.

Please let us know if this helps!

I have to take a break from this part of the project, but i will update in a week or two.

Thanks again.