Problem to refresh/redraw Graph | Cytoscape

Hi everyone!

I’m new to dash and cytoscope, but I’ve been playing with some code for a few days and I’m stuck now. I have a graph with define width and height (style), but I need to modify that dimensions and draw a new graph in that space, however, despite the container change the size, the graph itself doesn’t, It still have the previous dimensions so the graph is outside the container. I use a callback for this change and also place an Interval to force the refresh, and It worked, but now I’ve added another graph (inside a different container) that is also populated in the same callback, but the data for this new graph is quite large and the interval was causing kind of an infinite loop, so I removed the Interval and once again I have the same problem, the graph is outside the container after It resizes. I have the responsive flag activated but still no luck.

My last try was to create a new hide container for the the graph I was trying to resize and when I want to show this new graph, I hide the old one. However, once again my graph is not showing properly, when I unhide the container for the first time is empty. So I decide, just to test this hide/unhide feature, to add a button to hide/unhide the original graph, and I can replicate this issue.

This is how I define the graph showed below:

        html.Button('Pausar para Mover/Actualizar', id='start-stop-button', style={'width': '15%','display': 'inline', }, ),
        html.Div(id = "capa_P_container", style={}, children=[
            cyto.Cytoscape(
                id='Capa_P',
                layout={'name': 'preset','fit': True, },
                style={'width': '100%', 'height': '94vh'},
                stylesheet=vars.my_stylesheet,
                elements=vars.nodes + edges,
                autoRefreshLayout = True,
                responsive = True,
            ),
        ]),

vmplayer_obYUrVwDX1

Thanks in advance.

Hello @AReyes,

Welcome to the community!

It might be helpful and help you get answers if you could provide an MRE to recreate this in a smaller part:

Sure, below you can find two examples, as an insight, I read another thread where the user had the same issue, and found out that the cause was the flag “responsive=True”, I tried removing it and yes, at least for example 1 the graph hides/unhides correctly.

Example 1: Hidding the graph with a button

Example 1
from dash import Dash, html, dcc, Input, Output, callback, State
import dash_cytoscape as cyto
import dash_bootstrap_components as dbc

external_stylesheets=[dbc.themes.GRID]

cyto.load_extra_layouts()

app = Dash(__name__, external_stylesheets=external_stylesheets)

nodes = [
    {
        'data': {'id': short, 'label': label},
        'position': {'x': 20 * lat, 'y': -20 * long}
    }
    for short, label, long, lat in (
        ('la', 'Los Angeles', 34.03, -118.25),
        ('nyc', 'New York', 40.71, -74),
        ('to', 'Toronto', 43.65, -79.38),
        ('mtl', 'Montreal', 45.50, -73.57),
        ('van', 'Vancouver', 49.28, -123.12),
        ('chi', 'Chicago', 41.88, -87.63),
        ('bos', 'Boston', 42.36, -71.06),
        ('hou', 'Houston', 29.76, -95.37)
    )
]

edges = [
    {'data': {'source': source, 'target': target}}
    for source, target in (
        ('van', 'la'),
        ('la', 'chi'),
        ('hou', 'chi'),
        ('to', 'mtl'),
        ('mtl', 'bos'),
        ('nyc', 'bos'),
        ('to', 'hou'),
        ('to', 'nyc'),
        ('la', 'nyc'),
        ('nyc', 'bos')
    )
]

elements = nodes + edges

app.layout = dbc.Container([
    dbc.Row([
        dbc.Col(html.Button('Click', id='start-stop-button', style={'width': '15%','display': 'inline', }, ))
        ]),
    dbc.Row([
        dbc.Col(
            cyto.Cytoscape(
                id='cytoscape-layout-1',
                elements=elements,
                style={'width': '50vw', 'height': '50vh','border': '2px solid black'},
                layout={
                    'name': 'preset', 'fit':True,
                },
                responsive=True,
            ),id='col_graph',style={'border': '2px solid red'})],id='row_graph',style={'border': '2px solid green'}),
],fluid=True,)

@callback(
    Output('cytoscape-layout-1', 'style'),
    [Input('start-stop-button', 'n_clicks')],
    [State('cytoscape-layout-1', 'style')],prevent_initial_call=True
)
def hidding(button_clicks, cyto_style):
    if button_clicks % 2:
        cyto_style = {'width': '50vw', 'height': '50vh', 'display': 'none'}
        
        return cyto_style
    else:
        cyto_style = {'width': '50vw', 'height': '50vh','border': '2px solid black'}
        return cyto_style

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

Example 2: Changing the graph content and size depending on match from a text box (when match you have to zoom out to find the graph)

Example 2
from dash import Dash, html, dcc, Input, Output, callback, State
import dash_cytoscape as cyto
import dash_bootstrap_components as dbc

external_stylesheets=[dbc.themes.GRID]

cyto.load_extra_layouts()

app = Dash(__name__, external_stylesheets=external_stylesheets)

nodes = [
    {
        'data': {'id': short, 'label': label},
        'position': {'x': 20 * lat, 'y': -20 * long}
    }
    for short, label, long, lat in (
        ('la', 'Los Angeles', 34.03, -118.25),
        ('nyc', 'New York', 40.71, -74),
        ('to', 'Toronto', 43.65, -79.38),
        ('mtl', 'Montreal', 45.50, -73.57),
        ('van', 'Vancouver', 49.28, -123.12),
        ('chi', 'Chicago', 41.88, -87.63),
        ('bos', 'Boston', 42.36, -71.06),
        ('hou', 'Houston', 29.76, -95.37)
    )
]

edges = [
    {'data': {'source': source, 'target': target}}
    for source, target in (
        ('van', 'la'),
        ('la', 'chi'),
        ('hou', 'chi'),
        ('to', 'mtl'),
        ('mtl', 'bos'),
        ('nyc', 'bos'),
        ('to', 'hou'),
        ('to', 'nyc'),
        ('la', 'nyc'),
        ('nyc', 'bos')
    )
]

elements = nodes + edges

app.layout = dbc.Container([
    dbc.Row([
        dbc.Col(
            html.Div(style={'width': '50%', 'display': 'inline', 'padding': '5px', 'font-weight': 'bold'}, children=[
            'Code: ',
            dbc.Input(id='input_code', type='text', style={'width': '10%'})
        ]))
        ]),
    dbc.Row([
        dbc.Col(
            cyto.Cytoscape(
                id='cytoscape-layout-1',
                elements=elements,
                style={'width': '50vw', 'height': '50vh','border': '2px solid black'},
                layout={
                    'name': 'preset', 'fit':True,
                },
                responsive=True,
            ),id='col_graph',style={'border': '2px solid red'})],id='row_graph',style={'border': '2px solid green'}),
],fluid=True,)

@callback(Output('col_graph', 'children'),
        Input('input_code', 'value'),prevent_initial_call=True)
def match_code(code):
    if code is None or code == "":
        return [
            cyto.Cytoscape(
                id='cytoscape-layout-1',
                layout={'name': 'preset', 'fit':True,},
                style={'width': '50vw', 'height': '50vh','border': '2px solid black'},
                elements=elements,
                responsive = True,
        )]
    else:
        nodes = [
            {
                'data': {'id': short, 'label': label},
                'position': {'x': 20 * lat, 'y': -20 * long}
            }
            for short, label, long, lat in (
                ('la', 'Los Angeles', 34.03, -118.25),
                ('nyc', 'New York', 40.71, -74),
                ('to', 'Toronto', 43.65, -79.38),
                ('mtl', 'Montreal', 45.50, -73.57),
            )
        ]

        edges = [
            {'data': {'source': source, 'target': target}}
            for source, target in (
                ('to', 'mtl'),
                ('to', 'nyc'),
                ('la', 'nyc'),
            )
        ]       

        elements_2 = nodes + edges

        return [
            cyto.Cytoscape(
                id='cytoscape-layout-1',
                layout={'name': 'preset', 'fit':True,},
                style={'width': '50vw', 'height': '25vh','border': '2px solid black'},
                elements=elements_2,
                responsive = True,
        )]


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

Hope you can find the problem.

Thanks in advance.

Here, check this:

from dash import Dash, html, dcc, Input, Output, callback, State
import dash_cytoscape as cyto
import dash_bootstrap_components as dbc

external_stylesheets=[dbc.themes.GRID]

cyto.load_extra_layouts()

app = Dash(__name__, external_stylesheets=external_stylesheets)

nodes = [
    {
        'data': {'id': short, 'label': label},
        'position': {'x': 20 * lat, 'y': -20 * long}
    }
    for short, label, long, lat in (
        ('la', 'Los Angeles', 34.03, -118.25),
        ('nyc', 'New York', 40.71, -74),
        ('to', 'Toronto', 43.65, -79.38),
        ('mtl', 'Montreal', 45.50, -73.57),
        ('van', 'Vancouver', 49.28, -123.12),
        ('chi', 'Chicago', 41.88, -87.63),
        ('bos', 'Boston', 42.36, -71.06),
        ('hou', 'Houston', 29.76, -95.37)
    )
]

edges = [
    {'data': {'source': source, 'target': target}}
    for source, target in (
        ('van', 'la'),
        ('la', 'chi'),
        ('hou', 'chi'),
        ('to', 'mtl'),
        ('mtl', 'bos'),
        ('nyc', 'bos'),
        ('to', 'hou'),
        ('to', 'nyc'),
        ('la', 'nyc'),
        ('nyc', 'bos')
    )
]

elements = nodes + edges

app.layout = dbc.Container([
    dbc.Row([
        dbc.Col(
            html.Div(style={'width': '50%', 'display': 'inline', 'padding': '5px', 'font-weight': 'bold'}, children=[
            'Code: ',
            dbc.Input(id='input_code', type='text', style={'width': '10%'})
        ]))
        ]),
    dbc.Row([
        dbc.Col(
            cyto.Cytoscape(
                id='cytoscape-layout-1',
                elements=elements,
                style={'width': '50vw', 'height': '50vh','border': '2px solid black'},
                layout={
                    'name': 'preset', 'fit':True,
                },
                responsive=True,
            ),id='col_graph',style={'border': '2px solid red'})],id='row_graph',style={'border': '2px solid green'}),
],fluid=True,)

@callback(Output('col_graph', 'children'),
        Input('input_code', 'value'),prevent_initial_call=True)
def match_code(code):
    if code is None or code == "":
        return [
            cyto.Cytoscape(
                id='cytoscape-layout-2',
                layout={'name': 'preset', 'fit':True,},
                style={'width': '50vw', 'height': '50vh','border': '2px solid black'},
                elements=elements,
                responsive = True,
        )]
    else:
        nodes = [
            {
                'data': {'id': short, 'label': label},
                'position': {'x': 20 * lat, 'y': -20 * long}
            }
            for short, label, long, lat in (
                ('la', 'Los Angeles', 34.03, -118.25),
                ('nyc', 'New York', 40.71, -74),
                ('to', 'Toronto', 43.65, -79.38),
                ('mtl', 'Montreal', 45.50, -73.57),
            )
        ]

        edges = [
            {'data': {'source': source, 'target': target}}
            for source, target in (
                ('to', 'mtl'),
                ('to', 'nyc'),
                ('la', 'nyc'),
            )
        ]

        elements_2 = nodes + edges

        return [
            cyto.Cytoscape(
                id=f'cytoscape-layout-{code}',
                layout={'name': 'preset', 'fit':True,},
                style={'width': '50vw', 'height': '25vh','border': '2px solid black'},
                elements=elements_2,
                responsive = True,
        )]


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

I believe the issue stems from the fact that even though you are passing the cytoscape like it is a new component, the props on the dash app dont treat it as new, thus changing the id cause it to be a completely new cytoscape.

Once you do this, it starts treating these as the initial load that you are expecting, otherwise it is just passing updates to the component.

Oh that’s interesting - you’re right @jinnyzor, changing the ID causes a fresh instance of the component instead of updating the props on the existing component. If this has an effect yet we didn’t explicitly ask the component to hold onto this state (using a feature like persisted_props, which dash-cytoscape doesn’t support anyway, but some components have built-in ways to hold some state) this seems like a bug, and we need to figure out whether this is always a bug or if there are cases this behavior is desirable and we need a way to distinguish between them.

Thanks @alexcjohnson,

To put another instance where this is an issue would be with AG Grid. :wink:

If using a path_template where all you are doing is switching between the variables, the grid fights to keep the columns the same and the order. Changing the id again is a fix, I use pattern-matching for this.

Right, this is like what plotly.js (ie dcc.Graph) does with uirevision - though this makes no difference until the user has explicitly done something to the graph like zoomed in or clicked a legend item or something, then if uirevision is defined and doesn’t change we try and replay those actions when we receive new figure data. But that’s opt-in behavior, without opting in it’s a bug if the component holds onto state that’s not present in the props, so the end result should be identical when you pass a complete component whether the ID is the same or not.

One simple thing we could do to avoid these situations is add a prop like reset_view that defaults to True but during render the component (after dutifully clearing whatever state it was holding) sets it to False so that later when callbacks change individual props this state will be maintained, but whenever you pass in a complete component, as long as you don’t explicitly set reset_view=False, you can be confident in getting the same result no matter what happened before. Simple conceptually anyway, might be tricky to find all the state we need to clear!

Maybe for these specific things, there is an api to destroy the component and then start from fresh.

I suppose another hacky workaround would be to wrap the offending component in a html.Div and change the ID of that instead… that way you could still have callbacks attached to your main component but the new wrapper ID should ensure that react fully destroys and recreates the component. But again, hack, not a great solution.

I like that work around, that way you dont break the callbacks and have to work with pattern-matching.

Ohhh what a nice discussion, I’ve test de the workaround and It works.

So, in conclussion, It’s not possible to update an already created cytoscape. If any change occurs, a new one must be created. Because in Example 1 I was trying to hide the cytoscape changing the style with ‘display’: ‘none’, but after first hide the cytoscape kind of dissapear.

1 Like

Could you please give an example of how to do this, I was reading about Pattern Matching Callbacks but is not complete clear for me, as all the examples change the indexes using the n_clicks property from a button, and in my case I’m using an Input text box.

This is the link I was reading from: Pattern-Matching Callbacks | Dash for Python Documentation | Plotly

Hello @AReyes,

Do you need callbacks to be able to trigger from the cytoscape?