Add/Remove elements without re-rendering the graph

Hi! I am recently working on an app using plotly dash. And I want to add/remove elements without changing other nodes/edges location. For the example on the website, every time when I click on add or remove elements, the whole graph regenerate and all of the node are re-arraged. (Callbacks | Dash for Python Documentation | Plotly) I am wondering if there is a way to only update the certain location of the node or edge without regenerating the whole map. Thank you to all!

To be more specific, the code is the same as the example.

import dash
import dash_cytoscape as cyto
import dash_html_components as html
import dash_core_components as dcc
from pprint import pprint
from dash.dependencies import Input, Output, State

app = dash.Dash(__name__)


nodes = [
    {
        'data': {'id': short, 'label': label},
        'position': {'x': 20*lat, 'y': -20*long}
    }
    for short, label, long, lat in (
        ('la', 'Los Angeles', 34.03, -118.25),
        ('nyc', 'New York', 40.71, -74),
        ('to', 'Toronto', 43.65, -79.38),
        ('mtl', 'Montreal', 45.50, -73.57),
        ('van', 'Vancouver', 49.28, -123.12),
        ('chi', 'Chicago', 41.88, -87.63),
        ('bos', 'Boston', 42.36, -71.06),
        ('hou', 'Houston', 29.76, -95.37)
    )
]

edges = [
    {'data': {'source': source, 'target': target}}
    for source, target in (
        ('van', 'la'),
        ('la', 'chi'),
        ('hou', 'chi'),
        ('to', 'mtl'),
        ('mtl', 'bos'),
        ('nyc', 'bos'),
        ('to', 'hou'),
        ('to', 'nyc'),
        ('la', 'nyc'),
        ('nyc', 'bos')
    )
]


default_stylesheet = [
    {
        'selector': 'node',
        'style': {
            'background-color': '#BFD7B5',
            'label': 'data(label)'
        }
    },
    {
        'selector': 'edge',
        'style': {
            'line-color': '#A3C4BC'
        }
    }
]


app.layout = html.Div([
    html.Div([
        html.Button('Add Node', id='btn-add-node', n_clicks_timestamp=0),
        html.Button('Remove Node', id='btn-remove-node', n_clicks_timestamp=0)
    ]),

    cyto.Cytoscape(
        id='cytoscape-elements-callbacks',
        layout={'name': 'cose'},
        stylesheet=default_stylesheet,
        style={'width': '100%', 'height': '450px'},
        elements=edges+nodes
    )
])


@app.callback(Output('cytoscape-elements-callbacks', 'elements'),
              Input('btn-add-node', 'n_clicks_timestamp'),
              Input('btn-remove-node', 'n_clicks_timestamp'),
              State('cytoscape-elements-callbacks', 'elements'))
def update_elements(btn_add, btn_remove, elements):
    current_nodes, deleted_nodes = get_current_and_deleted_nodes(elements)
    # If the add button was clicked most recently and there are nodes to add
    if int(btn_add) > int(btn_remove) and len(deleted_nodes):

        # We pop one node from deleted nodes and append it to nodes list.
        current_nodes.append(deleted_nodes.pop())
        # Get valid edges -- both source and target nodes are in the current graph
        cy_edges = get_current_valid_edges(current_nodes, edges)
        return cy_edges + current_nodes

    # If the remove button was clicked most recently and there are nodes to remove
    elif int(btn_remove) > int(btn_add) and len(current_nodes):
            current_nodes.pop()
            cy_edges = get_current_valid_edges(current_nodes, edges)
            return cy_edges + current_nodes

    # Neither have been clicked yet (or fallback condition)
    return elements

def get_current_valid_edges(current_nodes, all_edges):
    """Returns edges that are present in Cytoscape:
    its source and target nodes are still present in the graph.
    """
    valid_edges = []
    node_ids = {n['data']['id'] for n in current_nodes}

    for e in all_edges:
        if e['data']['source'] in node_ids and e['data']['target'] in node_ids:
            valid_edges.append(e)
    return valid_edges

def get_current_and_deleted_nodes(elements):
    """Returns nodes that are present in Cytoscape and the deleted nodes
    """
    current_nodes = []
    deleted_nodes = []

    # get current graph nodes
    for ele in elements:
        # if the element is a node
        if 'source' not in ele['data']:
            current_nodes.append(ele)

    # get deleted nodes
    node_ids = {n['data']['id'] for n in current_nodes}
    for n in nodes:
        if n['data']['id'] not in node_ids:
            deleted_nodes.append(n)

    return current_nodes, deleted_nodes

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

What I want to do is that in some cases, I might drag some node somewhere that I want it to be. And then when I click on add or remove node, the whole graph refresh and they all go to another place. For example, I want node A on the top left of the graph. And then when I click remove node, the node A goes somewhere else and I need to re-drag it to the top left corner. Is there a way that after I drag node A, adding or removing nodes do not change the location of the node A? Thank you to all!

This is not a complete answer because I don’t know what your code is like but have you looked into using states? They prevent the app from being reactive until something else happens. I don’t know how you’re keeping track of your edges but maybe the elements you’re adding/removing can go into a state and then have those affect only the nodes/edges of interest.

Thank you very much! I add more details there. Could you explain more about how to use state to handle this?

Hello yueqingfeng. Welcome to the forum.

Please when you post your code, inset this using the button from the tools panel to insert code snippets:

It will be much easier for others to look at your code or to try to run it.

I made this for you.

import dash
import dash_cytoscape as cyto
import dash_html_components as html
import dash_core_components as dcc
from pprint import pprint
from dash.dependencies import Input, Output, State

app = dash.Dash(name)

nodes = [
{
‘data’: {‘id’: short, ‘label’: label},
‘position’: {‘x’: 20lat, ‘y’: -20long}
}
for short, label, long, lat in (
(‘la’, ‘Los Angeles’, 34.03, -118.25),
(‘nyc’, ‘New York’, 40.71, -74),
(‘to’, ‘Toronto’, 43.65, -79.38),
(‘mtl’, ‘Montreal’, 45.50, -73.57),
(‘van’, ‘Vancouver’, 49.28, -123.12),
(‘chi’, ‘Chicago’, 41.88, -87.63),
(‘bos’, ‘Boston’, 42.36, -71.06),
(‘hou’, ‘Houston’, 29.76, -95.37)
)
]

edges = [
{‘data’: {‘source’: source, ‘target’: target}}
for source, target in (
(‘van’, ‘la’),
(‘la’, ‘chi’),
(‘hou’, ‘chi’),
(‘to’, ‘mtl’),
(‘mtl’, ‘bos’),
(‘nyc’, ‘bos’),
(‘to’, ‘hou’),
(‘to’, ‘nyc’),
(‘la’, ‘nyc’),
(‘nyc’, ‘bos’)
)
]

default_stylesheet = [
{
‘selector’: ‘node’,
‘style’: {
‘background-color’: ‘#BFD7B5’,
‘label’: ‘data(label)’
}
},
{
‘selector’: ‘edge’,
‘style’: {
‘line-color’: ‘#A3C4BC’
}
}
]

app.layout = html.Div([
html.Div([
html.Button(‘Add Node’, id=‘btn-add-node’, n_clicks_timestamp=0),
html.Button(‘Remove Node’, id=‘btn-remove-node’, n_clicks_timestamp=0)
]),

cyto.Cytoscape(
    id='cytoscape-elements-callbacks',
    layout={'name': 'cose'},
    stylesheet=default_stylesheet,
    style={'width': '100%', 'height': '450px'},
    elements=edges+nodes
)
])

@app.callback(Output(‘cytoscape-elements-callbacks’, ‘elements’),
Input(‘btn-add-node’, ‘n_clicks_timestamp’),
Input(‘btn-remove-node’, ‘n_clicks_timestamp’),
State(‘cytoscape-elements-callbacks’, ‘elements’))
def update_elements(btn_add, btn_remove, elements):
current_nodes, deleted_nodes = get_current_and_deleted_nodes(elements)
# If the add button was clicked most recently and there are nodes to add
if int(btn_add) > int(btn_remove) and len(deleted_nodes):

    # We pop one node from deleted nodes and append it to nodes list.
    current_nodes.append(deleted_nodes.pop())
    # Get valid edges -- both source and target nodes are in the current graph
    cy_edges = get_current_valid_edges(current_nodes, edges)
    return cy_edges + current_nodes

# If the remove button was clicked most recently and there are nodes to remove
elif int(btn_remove) > int(btn_add) and len(current_nodes):
        current_nodes.pop()
        cy_edges = get_current_valid_edges(current_nodes, edges)
        return cy_edges + current_nodes

# Neither have been clicked yet (or fallback condition)
return elements
def get_current_valid_edges(current_nodes, all_edges):
“”“Returns edges that are present in Cytoscape:
its source and target nodes are still present in the graph.
“””
valid_edges =
node_ids = {n[‘data’][‘id’] for n in current_nodes}

for e in all_edges:
    if e['data']['source'] in node_ids and e['data']['target'] in node_ids:
        valid_edges.append(e)
return valid_edges
def get_current_and_deleted_nodes(elements):
“”“Returns nodes that are present in Cytoscape and the deleted nodes
“””
current_nodes =
deleted_nodes =

# get current graph nodes
for ele in elements:
    # if the element is a node
    if 'source' not in ele['data']:
        current_nodes.append(ele)

# get deleted nodes
node_ids = {n['data']['id'] for n in current_nodes}
for n in nodes:
    if n['data']['id'] not in node_ids:
        deleted_nodes.append(n)

return current_nodes, deleted_nodes
if name == ‘main’:
app.run_server(debug=True)
1 Like

Oh I am sorry about it! I have changed it using the code snippet. Thank you for telling me this!

1 Like