Cytoscape Clientside

Hey,
I cannot really explain this behaviour, might be a bug:
Normal Callbacks works just fine with selecting Nodes. Clientside Callback does not work even though the node has ‘selected’ as classes afterwards. Additionally if you click first on the clientside callback and then on the normal it does not work anymore. Here is a MRE:

from dash import html, Output,State, Input, Dash, no_update ,dcc
import dash_cytoscape as cyto

app = Dash(__name__)

app.layout = html.Div([
    cyto.Cytoscape(
        id='cytoscape',
        style={'width': '100%', 'height': '400px'},
        stylesheet = [
            {
                'selector': 'node',
                'style': {
                    'label': 'data(label)',
                    'font-size': '50px',
                    'text-valign': 'center',
                    'text-halign': 'right',
                    'color': 'white',
                    'text-outline-width': '2px',
                    "text-wrap": "wrap",
                    'width': 'data(size)',
                    'height': 'data(size)',
                    "text-wrap": "wrap"
                }},
                
                {'selector': '.selected',
                'style': {
                    'border-width': '8px',
                    'font-size': '80px',
                    'font-weight': 'bold',
                    'text-outline-width': '4px',
                    'background-image': 'data(url)',
                    'background-fit': 'cover',
                }},
                
            ],
        elements=[
            # Define your nodes here with unique IDs
            {'data': {'id': 'node1','size':'150px', 'label': 'Node 1', 'url': 'https://i.scdn.co/image/ab67616d00001e0216aaf05fe82237576a7d0e38'}, 'position': {'x': 50, 'y': 50}},
            {'data': {'id': 'node2','size':'150px', 'label': 'Node 2', 'url': 'https://i.scdn.co/image/ab67616d00001e0216aaf05fe82237576a7d0e38'}, 'position': {'x': 150, 'y': 150}},
            {'data': {'id': 'node3','size':'150px', 'label': 'Node 3', 'url': 'https://i.scdn.co/image/ab67616d00001e0216aaf05fe82237576a7d0e38'}, 'position': {'x': 250, 'y': 250}},
        ],
    ),
    html.Button('Normal Callback', id='normal-button'),
    html.Button('Client-Side Callback', id='client-button'),
])

@app.callback(
    Output('cytoscape', 'elements' ,allow_duplicate=True),
    Input('normal-button', 'n_clicks'),
    State('cytoscape', 'elements'),
    prevent_initial_call = True

)
def update_output(n_clicks, elements):
    if n_clicks:
        elements[0]['classes'] = 'selected'
        print(elements[0])
        return elements

app.clientside_callback(
    """
    function(n_clicks, elements) {
        if (n_clicks) {
            elements[0].classes = 'selected';
            console.log(elements[0]);
            return elements;
        }
        return dash_clientside.no_update;
    }
    """,
    Output('cytoscape', 'elements'),
    Input('client-button', 'n_clicks'),
    State('cytoscape', 'elements'),
    prevent_initial_call = True
)

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

Additional Question: Is there something like Patch() (Partial Property Updates) for Clientside Callbacks?

Hello @Louis,

There isnt a need for Patch for clientside callbacks, this was created for network load.

You can manipulate the object that you want to return by pulling it in as a State and changing it there. :slight_smile:

For Dash to recognize the change, you need to make it a new object, give this a try:

from dash import html, Output,State, Input, Dash, no_update ,dcc
import dash_cytoscape as cyto

app = Dash(__name__)

app.layout = html.Div([
    cyto.Cytoscape(
        id='cytoscape',
        style={'width': '100%', 'height': '400px'},
        stylesheet = [
            {
                'selector': 'node',
                'style': {
                    'label': 'data(label)',
                    'font-size': '50px',
                    'text-valign': 'center',
                    'text-halign': 'right',
                    'color': 'white',
                    'text-outline-width': '2px',
                    "text-wrap": "wrap",
                    'width': 'data(size)',
                    'height': 'data(size)',
                    "text-wrap": "wrap"
                }},
                
                {'selector': '.selected',
                'style': {
                    'border-width': '8px',
                    'font-size': '80px',
                    'font-weight': 'bold',
                    'text-outline-width': '4px',
                    'background-image': 'data(url)',
                    'background-fit': 'cover',
                }},
                
            ],
        elements=[
            # Define your nodes here with unique IDs
            {'data': {'id': 'node1','size':'150px', 'label': 'Node 1', 'url': 'https://i.scdn.co/image/ab67616d00001e0216aaf05fe82237576a7d0e38'}, 'position': {'x': 50, 'y': 50}},
            {'data': {'id': 'node2','size':'150px', 'label': 'Node 2', 'url': 'https://i.scdn.co/image/ab67616d00001e0216aaf05fe82237576a7d0e38'}, 'position': {'x': 150, 'y': 150}},
            {'data': {'id': 'node3','size':'150px', 'label': 'Node 3', 'url': 'https://i.scdn.co/image/ab67616d00001e0216aaf05fe82237576a7d0e38'}, 'position': {'x': 250, 'y': 250}},
        ],
    ),
    html.Button('Normal Callback', id='normal-button'),
    html.Button('Client-Side Callback', id='client-button'),
])

@app.callback(
    Output('cytoscape', 'elements' ,allow_duplicate=True),
    Input('normal-button', 'n_clicks'),
    State('cytoscape', 'elements'),
    prevent_initial_call = True

)
def update_output(n_clicks, elements):
    if n_clicks:
        elements[0]['classes'] = 'selected'
        print(elements[0])
        return elements

app.clientside_callback(
    """
    function(n_clicks, elements) {
        if (n_clicks) {
            elements[0].classes = 'selected';
            console.log(elements[0]);
            return JSON.parse(JSON.stringify(elements));
        }
        return dash_clientside.no_update;
    }
    """,
    Output('cytoscape', 'elements'),
    Input('client-button', 'n_clicks'),
    State('cytoscape', 'elements'),
    prevent_initial_call = True
)

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

Hey, thanks for your response.
Makes sense that there is no Patch for clientside callbacks.

That is a good approach, unfortunately it did not work for me, not even in this MRE.

What exactly did not work? Did you get an error?

No, I did not get an error it simpy does not show the background image, as it should when the node is selected. It works well with the normal callback, but not with the clientside callback. And when I first press the clientside and then the normal it does not work anymore with the normal.