Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update cycle broken after callback #159

Open
maxkfranz opened this issue Oct 25, 2021 · 3 comments
Open

Update cycle broken after callback #159

maxkfranz opened this issue Oct 25, 2021 · 3 comments

Comments

@maxkfranz
Copy link

Description

After running a callback with Output and State on the graph JSON, the update cycle is broken.

This affects Scale AI and Hydro Quebec.

CC: @mtwichan @alexcjohnson @mj3cheun @jackparmer

Steps/Code to Reproduce

This is a minimal example adapted from another reproducible example from Matthew (@mtwichan). You can drag around the nodes in the demo to easily demonstrate the issue:

  1. Press the print-JSON button.
  2. Move a node around.
  3. Press the JSON button again.
  4. Note that the position changed, reflecting the drag.
  5. Press the break button, which has input/output arguments on the JSON.
  6. Move the node around again.
  7. Press the JSON button. The position is not updated this time, or any time after this.

Notes:

  • This is very likely to affect other JSON as well (e.g. data), though position is simple and clear to demonstrate.
  • This may need to be filed in Dash rather than here, as this may be a general issue with Dash.
  • You can drop this file into this repo in the root as something like usage.py to quickly verify.
import dash
import dash_cytoscape as cyto
import dash_html_components as html
from dash.dependencies import Input, Output, State
import json

from dash.exceptions import PreventUpdate

from copy import deepcopy

app = dash.Dash(__name__)
server = app.server

app.layout = html.Div([
    cyto.Cytoscape(
        id='cytoscape',
        elements=[
            {'data': {'id': 'one', 'label': 'Node 1'},
             'position': {'x': 50, 'y': 50}},
            {'data': {'id': 'two', 'label': 'Node 2'},
             'position': {'x': 200, 'y': 200}},
            {'data': {'source': 'one', 'target': 'two', 'label': '1 to 2'}}
        ],
        layout={'name': 'preset'}
    ),
    html.Button("Print elements JSONified", id="button-cytoscape"),
    html.Button("Break", id="button-break"),
    html.Div(id="html-cytoscape"),
])

@app.callback(
    Output("html-cytoscape", "children"),
    [Input("button-cytoscape", "n_clicks")],
    [State("cytoscape", "elements")],
)

def testCytoscape(n_clicks, elements):
    if n_clicks:
        return json.dumps(elements)

@app.callback(
    Output("cytoscape", "elements"),
    [Input("button-break", "n_clicks")],
    [State("cytoscape", "elements")],
)

def breakCytoscape(n_clicks, elements):
    if n_clicks:
        return deepcopy(elements) # happens with a deep copy or not 
    else:
        raise PreventUpdate

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

Expected Results

The update cycle should continue.

Actual Results

The update cycle is stopped.

Versions

@mtwichan, would you post your versions here for reference?

@alexcjohnson
Copy link
Collaborator

I think I see what's going on here:

  • When we initially render the component, the elements array is used directly in the cytoscape instance.
  • That means that changes made in-place to that array internally by Cytoscape are reflected in the renderer's layout object.
  • But when we update elements, a new object is inserted into the renderer's layout but internally to the component the original object is patched to match the new object, rather than simply replaced by the new object.
  • So after an update - any update where a new elements array was created by a callback - the one inside the Cytoscape instance is no longer the same one as in layout so updates will not be reflected there.

Another thing to note - which I imagine you all are well aware of but it's a significant limitation of this component today - is that elements does not function as an Input. So if I take the first callback above and change it to:

@app.callback(
    Output("html-cytoscape", "children"),
    Input("button-cytoscape", "n_clicks"),
    Input("cytoscape", "elements"),
)
def testCytoscape(n_clicks, elements):
    if n_clicks:
        return json.dumps(elements)

Ideally you'd be able to click the JSON button (to get the json to display at all) and then every time a node is moved you would see the new positions. But that doesn't happen: to get the updated positions to show up you have to click the JSON button again.

The ideal solution here is to watch events that cause changes in elements, and have the component call setProps({elements: newElements}) - that would propagate the changes back into layout, fixing the case raised in this PR and allowing elements to be used as an Input.

@maxkfranz
Copy link
Author

maxkfranz commented Oct 27, 2021 via email

@marc-vdm
Copy link

Hi all,

I've been looking to better understand my issue (#167), and I'm now thinking mine is a duplicate of this. Could you let me know if

  1. you also think this is a duplicate (then I'll close mine)?
  2. you managed to find a solution or workaround to this in the meantime?

My use-case really depends on being able to submit a new elements set during use, so not being able to do this would require me to find an alternative to this cytoscape library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants