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.

It’s funny, it does also not work with updating the stylesheet.

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-color': '#00FF33'
                }},
                
            ],
        elements=[
            # Define your nodes here with unique IDs
            {'data': {'id': 'node1','size':'150px', 'label': 'Node 1', }, 'position': {'x': 50, 'y': 50}},
            {'data': {'id': 'node2','size':'150px', 'label': 'Node 2', }, 'position': {'x': 150, 'y': 150}},
            {'data': {'id': 'node3','size':'150px', 'label': 'Node 3', }, 'position': {'x': 250, 'y': 250}, 'classes':'selected'},
        ],
    ),
    html.Button('Normal Callback', id='normal-button'),
    html.Button('Client-Side Callback', id='client-button'),
    html.Button('Validate Clientside', id='validate-button'),
])

@app.callback(
    Output('cytoscape', 'stylesheet' ,allow_duplicate=True),
    Input('normal-button', 'n_clicks'),
    State('cytoscape', 'stylesheet'),
    prevent_initial_call = True
)
def update_output(n_clicks, sheet):
    if n_clicks:
        sheet[1]['style']['background-color'] = '#444444'
        
        return sheet

app.clientside_callback(
    """
    function changeColor(n_clicks, sheet){
        if (n_clicks){
            sheet[1]['style']['background-color'] = '#444444';
            console.log(sheet[1]);
            return sheet;
        }else{
        return dash_clientside.no_update;
        }
    }
    """,
    Output('cytoscape', 'stylesheet'),
    Input('client-button', 'n_clicks'),
    State('cytoscape', 'stylesheet'),
    prevent_initial_call = True
)

@app.callback(
    Output('cytoscape', 'stylesheet' ,allow_duplicate=True),
    Input('validate-button', 'n_clicks'),
    State('cytoscape', 'stylesheet'),
    prevent_initial_call = True
)
def update_output(n_clicks, sheet):
    if n_clicks:     
        return sheet


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

With Heatmaps it does work with [quote=“jinnyzor, post:3, topic:78636”]
JSON.parse(JSON.stringify(elements)
[/quote]

from dash import Dash, html, dcc, Input, Output, State, no_update
import plotly.graph_objs as go


layer1_data = [[1,2,3], [None, 1, None]]

app = Dash(__name__)


app.layout = html.Div([
    html.H1("Heatmap Change"),
    dcc.Graph(id='heatmap-graph', figure = {
        'data': [
            go.Heatmap(z=layer1_data, colorscale='Viridis',  showscale=False)
        ]
    }),
    html.Button("Normal Update", id='normal-button'),
    html.Button("Clientside Update", id='clientside-button'),
    html.Button("Validate Clientside", id='validate-button'),
])

@app.callback(
    Output('heatmap-graph', 'figure', allow_duplicate=True),
    Input('normal-button', 'n_clicks'),
    State('heatmap-graph', 'figure'),
    prevent_initial_call = True
)
def change_hm(n, hm):
    if not n:
        return no_update
    else:
        hm['data'][0]['z']= [[1,2,3], [2, 1, 2]]
        return hm
    
@app.callback(
    Output('heatmap-graph', 'figure', allow_duplicate=True),
    Input('validate-button', 'n_clicks'),
    State('heatmap-graph', 'figure'),
    prevent_initial_call = True
)
def change_hm(n, hm):
    if not n:
        return no_update
    else:
        return hm

app.clientside_callback(
'''
function change_hm(n, hm){
    if (!n){
        return dash_clientside.no_update;
    }else{
        hm['data'][0]['z']= [[1,2,3], [2, 1, 2]];
        console.log(hm);
        return JSON.parse(JSON.stringify(hm));
    }
}
''',
    Output('heatmap-graph', 'figure'),
    Input('clientside-button', 'n_clicks'),
    State('heatmap-graph', 'figure'),
    prevent_initial_call = True
)

    

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

Hello @Louis,

For the heatmap, you need to make the returned figure a new object, other wise it is adjusting inplace and wont trigger a visual update.

1 Like

hey yes, I saw it right before you answered, it also works with the stylesheet now, thanks :slight_smile:
but with the elements its still not working :thinking:

selected could be a read-only prop.

but with the normal callback it works just fine?

So, its interesting, if you pass the string back to the prop, it runs, but then breaks the app…

1 Like

So adding elements in general does not seem to work. I’ll raise an issue on the repo. Maybe it can be fixed easily :slight_smile:

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

app = dash.Dash(__name__)

app.layout = html.Div([
    cyto.Cytoscape(
        id='cytoscape',
        elements=[
            {'data': {'id': 'node1', 'label':"node1"}, 'positions': {'x': 0, 'y': 0}}
        ],
        layout={'name': 'preset'},
        style={'width': '400px', 'height': '400px'}
    ),
    html.Button('Add Node', id='add-node-btn')
])


app.clientside_callback(
'''
function (nClicks, currentElements){
    if (!nClicks) {
        return dash_clientside.no_update;
    }
    let x=50;
    let y=100;
    let newNode = { 'data': { 'id': 'node2' }, 'position': { 'x': x, 'y': y } };
    currentElements.push(newNode);
    console.log(currentElements);

    return JSON.parse(JSON.stringify(currentElements));
}

''',
    Output('cytoscape', 'elements'),
    Input('add-node-btn', 'n_clicks'),
    State('cytoscape', 'elements'),
    prevent_initial_call=True
)

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

1 Like