tapNodeData not triggering after a specific n_clicks event

Hello everyone,

I try to make a cytoscape graph(based on an exemple from Dash) which can expand and collapse. I added a button “reset” which will collapse the graph to the first node. However if I use this button in a specific pattern, the tapNodeData input will not trigger anymore for the specific node.

It happen, when I click on the first node “TestCorp”, then when I click on “reset” button and when I want to re expand my graph by clicking on “TestCorp” node.

If I click on “reset” button then on node “TestCorp” it will work.

If I click on “TestCorp” node, then “aaa” node or “bbb” node, then “reset” button and finally on “TestCorp” node, the tapNodeData input will work.

You will find a test code below which reproduce my issue :

import requests
import dash
import dash_table
import pandas as pd
import pymongo
import datetime
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import json
import dash_cytoscape as cyto
from dash.dependencies import Input, Output, State

#enable svg
cyto.load_extra_layouts()

app = dash.Dash(__name__)

server = app.server
Data = []
Exemple_data = [
    {'provider': 'TestCorp', 'ID': 'V-aaaaa', 'Description': {'Name': 'aaa', 'Type': 'Vequipement', 'Region': 'us'}},
    {'provider': 'TestCorp', 'ID': 'V-bbbbb', 'Description': {'Name': 'bbb', 'Type': 'Vequipement', 'Region': 'us'}},
]


for x in Exemple_data:
    if x['provider'] == "TestCorp":
        
        Data.append([x['ID'],'TestCorp',x['Description']['Type'],x['Description']['Name']])

df = pd.DataFrame(Data,columns=['ID','Linked','Type','Name'])
nodes = set()

node_children = {}  # user id -> list of followers (cy_node format)

edge_children = {}  # user id -> list of cy edges ending at user id


cy_edges, cy_nodes = [], []

edges = df

colors = ['red', 'blue', 'green', 'yellow', 'pink']

###### Create first node
nodes.add('TestCorp')
cy_nodes.append({"data": {"id": 'TestCorp', "label": 'TestCorp',"Level": 0,"expanded": 'No'}})
for edge in edges.iterrows():
    if edge[1]['Linked'] != '':
        Type = edge[1]['Type']
        name = edge[1]['Name']
        source = edge[1]['ID']
        target = edge[1]['Linked']
        if edge[1]['Type'] == 'Vequipement':
            cy_source = {"data": {"id": source, "label": name,'Level':1,'Linked':target,'expanded':'No','Type':Type}}
        else:
            cy_source = {"data": {"id": source, "label": name,'Level':2,'Linked':target,'expanded':'No','Type':Type}}

        cy_target = {"data": {"id": target, "label": target,'Level':cy_source['data']['Level'],'expanded':'No'}}
        cy_edge = {'data': {'id': source+target, 'source': source, 'target': target, 'Level':cy_source['data']['Level'],'expanded':'No'}}

        if source not in nodes:  # Add the source node

            nodes.add(source)

            cy_nodes.append(cy_source)



        if target not in nodes:  # Add the target node

            nodes.add(target)

            cy_nodes.append(target)


    # Process dictionary of followers

    if not node_children.get(target):

        node_children[target] = []

    if not edge_children.get(target):

        edge_children[target] = []

    node_children[target].append(cy_source)

    edge_children[target].append(cy_edge)

genesis_node = cy_nodes[0]

genesis_node['classes'] = "genesis"

default_elements = [genesis_node]


# ################################# APP LAYOUT ################################

styles = {

    'json-output': {

        'overflow-y': 'scroll',

        'overflow-wrap': 'break-word',

        'height': 'calc(50% - 25px)',

        'border': 'thin lightgrey solid'

    },

    'tab': {'height': 'calc(98vh - 80px)'}

}


app.layout = html.Div([

    html.Button('Reset', id='bt-reset'),
    html.Div(id='my-output'),
    html.Div(className='eight columns', children=[

        cyto.Cytoscape(

            id='cytoscape',

            elements=default_elements,

            style={

                'height': '95vh',

                'width': 'calc(100% - 250px)',

                'float' : 'left',

            },
            

        )

    ])

])



########################### START OF DELETE FUNCTION ###########################
def delete_node(nodeData,elements):
    node_childrens = []
    if 'id' in nodeData:
        for linked in elements:
            if 'Linked' in linked['data'] and linked['data']['Linked'] == nodeData['id']:#####supression nodes
                node_childrens.append(linked)
            if 'target' in linked['data'] and linked['data']['target'] == nodeData['id']:####### supression edge
                node_childrens.append(linked)
    elif 'id' in nodeData['data']:
        for linked in elements:
            if 'Linked' in linked['data'] and linked['data']['Linked'] == nodeData['data']['id']:#####supression nodes
                node_childrens.append(linked)
            if 'target' in linked['data'] and linked['data']['target'] == nodeData['data']['id']:####### supression edge
                node_childrens.append(linked)
    if(node_childrens == []):
        return

    for children in node_childrens:
        delete_node(children,elements)
        elements.remove(children)
    i = 0
    for element in elements:
        if 'id' in nodeData:
            if element['data']['id'] == nodeData['id']:
                elements[i]['data']['expanded'] = 'No'
        elif 'id' in nodeData['data']:
            if element['data']['id'] == nodeData['data']['id']:
                elements[i]['data']['expanded'] = 'No'
        i+=1
    return 0
########################### 
END OF DELETE FUNCTION 
##########################

############################### 
CALLBACKS 
####################################

################### Callback n°1 Expand/Collapse nodes
@app.callback(Output("cytoscape", "elements"), [Input("cytoscape", "tapNodeData"),Input("bt-reset", "n_clicks")],State("cytoscape", "elements"))

def modification_on_elements(nodeData,n_clicks,elements):
    

    if not nodeData:
        return default_elements
    
    ctx = dash.callback_context
    
    if ctx.triggered:
        button_id = ctx.triggered[0]['prop_id'].split('.')[0]
        if button_id == "bt-reset":
            return default_elements
        elif button_id == "cytoscape":
            if 'Yes' == nodeData.get('expanded'):
                delete_node(nodeData,elements)
                return elements
            if 'No' == nodeData.get('expanded'):
                i = 0
                for element in elements:
                    if nodeData['id'] == element.get('data').get('id'):
                        elements[i]['data']['expanded'] = 'Yes'
                        break
                    i+=1
                
                node_childrens = node_children.get(nodeData['id'])
                    

                edge_childrens = edge_children.get(nodeData['id'])



                if node_childrens:

                    elements.extend(node_childrens)



                if edge_childrens:

                    elements.extend(edge_childrens)
    return elements
if __name__ == '__main__':

    app.run_server(debug=True)

Thank and have a nice day.

Duaran

Hi,

For people who might have the same problem, you will find how I resolve the problem :

To solve this, I add another callback which is triggered when i click on the reset button.

The output of the callback is tapNodeData and the callback return 0.