Dash_cytoscape with dcc.store, un-expected storage of position information

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)