Dash-Cytoscape doesn't display "children" nodes after deleting "parent" node

Issue:

In the code below, I want a user to be able to select any node in a Cytoscape graph and delete that node by clicking on a “Delete” button. This works for nodes that do not have children, however, when deleting a “parent” node, the associated “children” nodes are no longer displayed (even though they are still in the elements list for the Cytoscape component).

In the code, I delete the parent key for all children of the deleting parent node, and this is reflecting in the “print” statements of the elements list.

example_cyto


# Dash-related modules
from dash import Dash
import dash_cytoscape as cyto

import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate



app = Dash(__name__,
                title='Example Cyto',
                external_stylesheets=[dbc.themes.BOOTSTRAP],
                suppress_callback_exceptions=True)


elements =[
    # parent
    {'data':{'id':'node1', 'label':'node1'}, 'position':{'x':100, 'y':100}},
    # children
    {'data':{'id':'node1A', 'label':'node1A', 'parent':'node1'}, 'position':{'x':100, 'y':100}},
    {'data':{'id':'node1B', 'label':'node1B', 'parent':'node1'}, 'position':{'x':100, 'y':100}},
]

# Create cytoscape graph object
cytoscape = html.Div([
    cyto.Cytoscape(
        id='cytoscape',
        layout={'name': 'preset', 'fit':True},
        style={
            'width': '100vh', 
            'height': '600px'
            },
        responsive=True,
        autoRefreshLayout=True,
        elements=elements,
    )
], style={
    'display':'flex', 
    'width':'auto', 
    'height':'auto'
    })

app.layout = html.Div([

    dbc.Button("Delete node", id='delete-bt', color="danger"),
    cytoscape,
    dcc.Store('cytoscape-elements-store', data=elements)

])

# Update Cytoscape graph
@app.callback(
    Output("cytoscape", "elements"), 
    [Input("cytoscape-elements-store", "modified_timestamp")], 
    [State("cytoscape-elements-store", "data")])
def _update_cyto_elements(n, data):

    if n:
        data = data or []
        print(f"FINAL_ELEMENTS: {data}")
        return data

    else:
        raise PreventUpdate


# Update elements store
@app.callback(
    Output("cytoscape-elements-store", "data"), 
    [Input("delete-bt", "n_clicks"),],
    [State('cytoscape', 'tapNodeData'), State("cytoscape-elements-store", "data"),])
def _delete_node(n, tapNode, elements):

    if n and tapNode:

        el_id = tapNode['id'] 
        print(f"TAPNODE: {tapNode}")

        # Get list of selected node and children
        remove_idxs = []
        for i, el in enumerate(elements):
            if "source" not in el:
                # find selected node
                if el['data']['id'] == el_id:
                    remove_idxs.append(i)
                # find children of selected node and remove 'parent' key
                elif 'parent' in el['data']:
                    if el['data']['parent'] == el_id:
                        del el['data']['parent']

        elements = [el for el in elements if el['data']['id'] != el_id]

        return elements

    else:
        raise PreventUpdate


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


Hello,

I have the exact same problem.
I seems it is a problem inherent to Cytoscape.js because this section of the underlying Cytoscape.js API states that the parent/child relationship is immutable.

It was discussed in this issue on Github : Callbacks cannot remove and add nodes · Issue #106 · plotly/dash-cytoscape · GitHub
It was in 2020, I don’t know if it has been resolved since but it seems not.
Someone suggests a wordaround in the issue on Github, if it can help you.

Good luck,