You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We have a Dash App with two main callbacks and a container where we can see 3 different cytoscape graphs - only one at a time, the difference between the three graphs is the number of nodes (20, 30, 40)
Callback 1 saves the current position of the nodes and links (it saves the whole elements property) in a dcc.Store. That dcc.Store data property is a dict with one item for each cytoscape graph, so positions for each of the three graphs can be saved at the same time (saving positions of graph 2 doesn't overwrite saved positions for graph 1)
@app.callback(
Output('store', 'data'),
Input('save1, 'n_clicks'),
State('cyto', 'elements'),
State('number', 'value'),
State('store', 'data'),
)
def savemapstate(clicks,elements, number, store):
if clicks is None:
raise PreventUpdate
else:
store[number] = elements
return store
Callback 2 modifies the elements and layout properties of a cytoscape graph based on either (1) default value defined as a global variable, if the user clicks 'Reset' (2) saved value
@app.callback(
Output('cyto', 'elements'),
Output('cyto', 'layout'),
Input('update', 'n_clicks'),
Input('reset', 'n_clicks'),
State('number', 'value'),
State('store', 'data'),
prevent_initial_call=True
)
def updatemapstate(click1, click2, number, store):
triggered_id = callback_context.triggered[0]['prop_id'].split('.')[0]
if click1 is None and click2 is None:
raise PreventUpdate
else:
if "update" in triggered_id:
elements = store[number]
layout = {
'name': 'preset',
'fit': True,
}
elif "reset" in triggered_id:
elements = initial_data[number] # initial_data is a global variable (dict)
layout = {
'name': 'concentric',
'fit': True,
'minNodeSpacing': 100,
'avoidOverlap': True,
'startAngle': 50,
}
return elements, layout
When a user tries to update the
Sometimes it only happens when the number of nodes is >20; if the panning has been changed (the user has "dragged" the whole graph before saving the positions) the issue is worse (more nodes are displaced) and it can happen with <20 nodes.
dash-design-kit==1.6.8
dash==2.3.1 # it happens with 2.10.2 too
dash_cytoscape==0.2.0 # it happens with 0.3.0 too
pandas
gunicorn==20.0.4
pandas>=1.1.5
flask==2.2.5
Complete app code:
from dash import Dash, html, dcc, Input, Output, State, callback_context
from dash.exceptions import PreventUpdate
import dash_cytoscape as cyto
import json
import random
app = Dash(__name__)
data1 = [
{'data': {'id': f'{i}', 'label': f'Node {i}'}, 'position': {'x': 100*random.uniform(0,2), 'y': 100*random.uniform(0,2)}}
for i in range(20)] + [
{'data': {'id':f'link1-{i}','source': '1', 'target': f'{i}'}}
for i in range(2,20)
]
data2 = [
{'data': {'id': f'{i}', 'label': f'Node {i}'}, 'position': {'x': 100*random.uniform(0,2), 'y': 100*random.uniform(0,2)}}
for i in range(30)] + [
{'data': {'id':f'link1-{i}','source': '1', 'target': f'{i}'}}
for i in range(2,30)
]
data3 = [
{'data': {'id': f'{i}', 'label': f'Node {i}'}, 'position': {'x': 100*random.uniform(0,2), 'y': 100*random.uniform(0,2)}}
for i in range(40)] + [
{'data': {'id':f'link1-{i}','source': '1', 'target': f'{i}'}}
for i in range(2,40)
]
initial_data = {'1':data1, '2':data2, '3':data3}
app.layout = html.Div([
html.Div([
dcc.Dropdown(['1','2','3'], '1', id='number'),
html.Button(id='save', children='Save'),
html.Button(id='update', children='Update'),
html.Button(id='reset', children='Reset'),
dcc.Store(id='store', data={'1':[], '2':[], '3':[]})
],
style = {'width':'300px'}),
html.Div(
children=cyto.Cytoscape(
id='cyto',
layout={'name': 'concentric',},
panningEnabled=True,
zoom=0.5,
zoomingEnabled=True,
elements=[],
)
)
])
@app.callback(
Output('store', 'data'),
Input('save', 'n_clicks'),
State('cyto', 'elements'),
State('number', 'value'),
State('store', 'data'),
)
def savemapstate(clicks,elements, number, store):
if clicks is None:
raise PreventUpdate
else:
store[number] = elements
return store
@app.callback(
Output('cyto', 'elements'),
Output('cyto', 'layout'),
Input('update', 'n_clicks'),
Input('reset', 'n_clicks'),
State('number', 'value'),
State('store', 'data'),
prevent_initial_call=True
)
def updatemapstate(click1, click2, number, store):
triggered_id = callback_context.triggered[0]['prop_id'].split('.')[0]
if click1 is None and click2 is None:
raise PreventUpdate
else:
if "update" in triggered_id:
elements = store[number]
layout = {
'name': 'preset',
'fit': True,
}
elif "reset" in triggered_id:
elements = initial_data[number]
layout = {
'name': 'concentric',
'fit': True,
'minNodeSpacing': 100,
'avoidOverlap': True,
'startAngle': 50,
}
return elements, layout
if __name__ == '__main__':
app.run_server(debug=True)
Workaround
Returning in the callback a new cytoscape graph with a new id. If we return a cytoscape with the same id, the issue still happens.
To keep saving the elements (=use them as an Input in a callback) we can use pattern-matching callbacks
from dash import Dash, html, dcc, Input, Output, State, callback_context, ALL
from dash.exceptions import PreventUpdate
import dash_cytoscape as cyto
import json
import random
app = Dash(__name__)
data1 = [
{'data': {'id': f'{i}', 'label': f'Node {i}'}, 'position': {'x': 100*random.uniform(0,2), 'y': 100*random.uniform(0,2)}}
for i in range(20)] + [
{'data': {'id':f'link1-{i}','source': '1', 'target': f'{i}'}}
for i in range(2,20)
]
data2 = [
{'data': {'id': f'{i}', 'label': f'Node {i}'}, 'position': {'x': 100*random.uniform(0,2), 'y': 100*random.uniform(0,2)}}
for i in range(30)] + [
{'data': {'id':f'link1-{i}','source': '1', 'target': f'{i}'}}
for i in range(2,30)
]
data3 = [
{'data': {'id': f'{i}', 'label': f'Node {i}'}, 'position': {'x': 100*random.uniform(0,2), 'y': 100*random.uniform(0,2)}}
for i in range(40)] + [
{'data': {'id':f'link1-{i}','source': '1', 'target': f'{i}'}}
for i in range(2,40)
]
initial_data = {'1':data1, '2':data2, '3':data3}
app.layout = html.Div([
html.Div([
dcc.Dropdown(['1','2','3'], '1', id='number'),
html.Button(id='save', children='Save'),
html.Button(id='update', children='Update'),
html.Button(id='reset', children='Reset'),
dcc.Store(id='store', data={'1':[], '2':[], '3':[]})
],
style = {'width':'300px'}),
html.Div(
id='cyto-card',
children=[],
),
])
@app.callback(
Output('store', 'data'),
Input('save', 'n_clicks'),
State({'type':'cyto', 'index':ALL}, 'elements'),
State('number', 'value'),
State('store', 'data'),
)
def savemapsatae(clicks,elements, number, store):
if clicks is None:
raise PreventUpdate
else:
store[number] = elements[0]
return store
@app.callback(
Output('cyto-card', 'children'),
Input('update', 'n_clicks'),
Input('reset', 'n_clicks'),
State('number', 'value'),
State('store', 'data'),
prevent_initial_call=True
)
def updatemapsatae(click1, click2, number, store):
triggered_id = callback_context.triggered[0]['prop_id'].split('.')[0]
if click1 is None and click2 is None:
raise PreventUpdate
else:
if "update" in triggered_id:
elements = store[number]
layout = {
'name': 'preset',
}
elif "reset" in triggered_id:
elements = initial_data[number]
layout = {
'name': 'concentric',
'fit': True,
'minNodeSpacing': 100,
'avoidOverlap': True,
'startAngle': 50,
}
n = sum(filter(None, [click1, click2]))
cyto_return = cyto.Cytoscape(
id={'type':'cyto', 'index':n},
layout=layout,
panningEnabled=True,
zoom=0.5,
zoomingEnabled=True,
elements=elements,
)
return cyto_return
if __name__ == '__main__':
app.run_server(debug=True)
The text was updated successfully, but these errors were encountered:
Description
Screen.Recording.2023-07-21.at.16.16.43.mov
elements
property) in a dcc.Store. That dcc.Store data property is a dict with one item for each cytoscape graph, so positions for each of the three graphs can be saved at the same time (saving positions of graph 2 doesn't overwrite saved positions for graph 1)elements
andlayout
properties of a cytoscape graph based on either (1) default value defined as a global variable, if the user clicks 'Reset' (2) saved valueCode to Reproduce
Env: Python 3.8.12
requirements.txt:
Complete app code:
Workaround
elements
(=use them as an Input in a callback) we can use pattern-matching callbacksThe text was updated successfully, but these errors were encountered: