I have using following code to conduct test, the interval callback will show the latest data of “stored-positions”.
After the first time save cytoscape node position, the interval show the node latest location. However, if I adjust the node of cytoscape, the “stored-positions” changed without press “save-layout-bnt”
import dash
from dash import html, dcc, Input, Output, State, callback, clientside_callback
import dash_cytoscape as cyto
import json
\# Initialize the Dash app
app = dash.Dash(\__name_\_)
\# ---- Define the initial graph structure ----
\# Use the 'preset' layout to give nodes explicit starting positions
initial_elements = \[
{
'data': {'id': 'one', 'label': 'Node 1'},
'position': {'x': 50, 'y': 50}
},
{
'data': {'id': 'two', 'label': 'Node 2'},
'position': {'x': 250, 'y': 150}
},
{
'data': {'id': 'three', 'label': 'Node 3'},
'position': {'x': 150, 'y': 300}
},
{'data': {'source': 'one', 'target': 'two'}},
{'data': {'source': 'two', 'target': 'three'}}
\]
\# ---- Layout of the App ----
app.layout = html.Div(\[
html.H3("Dash Cytoscape: Store and Restore Node Positions"),
cyto.Cytoscape(
id='my-cytoscape',
elements=initial_elements,
layout={'name': 'preset'}, # Critical: 'preset' layout uses explicit positions
style={'width': '100%', 'height': '600px', 'border': '1px solid gray'},
\# Enable node dragging for user interaction
\# boxSelectionEnabled =True,
\# userPanningEnabled =True,
\# userZoomingEnabled =True
),
html.Br(),
html.Button('💾 Save Current Layout', id='save-layout-btn', n_clicks=0),
html.Button('🔄 Restore Saved Layout', id='restore-layout-btn', n_clicks=0),
html.Button('🔁 Reset to Initial Layout', id='reset-layout-btn', n_clicks=0),
html.Button('🔁 Load store data', id='load-btn', n_clicks=0),
dcc.Store(id='stored-positions', data={}), # Hidden store for saved positions
dcc.Store(id='initial-positions-store', data=initial_elements), # Hidden store for initial positions
html.Div(id='status-message', style={'marginTop': '10px', 'fontStyle': 'italic'}),
dcc.Store(id='save-event-tracker', data=0),
dcc.Interval(
id = "cytoscape_intervals",
interval=500,
n_intervals = 0,
),
\])
@callback(
Output('status-message', 'children', allow_duplicate=True),
Input('cytoscape_intervals', 'n_intervals'),
State('stored-positions', 'data'),
prevent_initial_call=True
)
def show_element_data(n_interval, store_data):
print (n_interval, store_data)
return dash.no_update
\# ---- 1. Clientside Callback: Save Current Positions to dcc.Store ----
\# This JavaScript runs in the browser to get the actual positions directly from the graph.
clientside_callback(
"""
function(click) {
// The 'cy' object is the Cytoscape.js instance
const cy = document.getElementById('my-cytoscape').\_cyreg.cy;
if (!cy) return window.dash_clientside.no_update;
const nodes = cy.nodes();
const positions = {};
nodes.forEach(node => {
positions\[node.id()\] = node.position();
});
// Return data to update the 'stored-positions' dcc.Store
console.log ("positions:", positions);
return positions;
}
""",
Output('stored-positions', 'data'),
Input('save-layout-btn', 'n_clicks'),
\# State('stored-positions', 'data'),
prevent_initial_call=True
)
\# ---- 3. Standard Callback: Reset Positions to Initial Layout ----
@callback(
Output('my-cytoscape', 'elements', allow_duplicate=True),
Input('reset-layout-btn', 'n_clicks'),
State('initial-positions-store', 'data'),
prevent_initial_call=True
)
def reset_layout(n_clicks, initial_elements_data):
"""Resets the graph to its initial defined elements and positions."""
return initial_elements_data
# ---- Run the Server ----
if \__name_\_ == '\__main_\_':
app.run(debug=True, port=7700)