Cytoscape Event Callbacks

In part 4, we showed how to update Cytoscape with other components by assigning callbacks that output to 'elements', 'stylesheet', 'layout'. Moreover, it is also possible to use properties of Cytoscape as an input to callbacks, which can be used to update other components, or Cytoscape itself. Those properties are updated (which fires the callbacks) when the user interact with elements in a certain way, which justifies the name of event callbacks. You can find props such as tapNode, which returns a complete description of the node object when the user clicks or taps on a node, mouseoverEdgeData, which returns only the data dictionary of the edge that was most recently hovered by the user. The complete list can be found in the Dash Cytoscape Reference.

Simple callback construction

Let's look back at the same city example as the previous chapter:

using Dash, DashCytoscape

app = dash()

nodes = [
    Dict(
        "data" =>  Dict("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 = [
    Dict("data" =>  Dict("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")
    )
]

default_stylesheet = [
    Dict(
        "selector" =>  "node",
        "style" =>  Dict(
            "background-color" =>  "#BFD7B5",
            "label" =>  "data(label)"
        )
    ),
    Dict(
        "selector" =>  "edge",
        "style" =>  Dict(
            "line-color" =>  "#A3C4BC"
        )
    )
]

app.layout = html_div([
    cyto_cytoscape(
        id="cytoscape-events",
        layout=Dict("name" =>  "preset"),
        elements=vcat(edges, nodes),
        stylesheet=default_stylesheet,
        style=Dict("width" =>  "100%", "height" =>  "450px")
    )
])

run_server(app, "0.0.0.0", debug=true)

This time, we will use the tapNodeData properties as input to our callbacks, which will simply dump the content into an html_pre:

using Dash, DashCytoscape

using JSON

app = dash()

styles = Dict(
    "pre" =>  Dict(
        "border" =>  "thin lightgrey solid",
        "overflowX" =>  "scroll"
    )
)

nodes = [
    Dict(
        "data" =>  Dict("id" =>  short, "label" =>  label),
        "position" =>  Dict("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 = [
    Dict("data" =>  Dict("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")
    )
]

default_stylesheet = [
    Dict(
        "selector" =>  "node",
        "style" =>  Dict(
            "background-color" =>  "#BFD7B5",
            "label" =>  "data(label)"
        )
    )
]

app.layout = html_div([
    cyto_cytoscape(
        id="cytoscape-event-callbacks-1",
        layout=Dict("name" =>  "preset"),
        elements=vcat(edges,nodes),
        stylesheet=default_stylesheet,
        style=Dict("width" =>  "100%", "height" =>  "450px")
    ),
    html_pre(id="cytoscape-tapNodeData-json", style=styles["pre"])
])

callback!(app,
    Output("cytoscape-tapNodeData-json", "children"),
    Input("cytoscape-event-callbacks-1", "tapNodeData")
) do data
    return JSON.json(data)
end

run_server(app, "0.0.0.0", debug=true)
null

Notice that the html_div is updated every time you click or tap a node, and returns the data dictionary of the node. Alternatively, you can use tapNode to obtain the entire element specification (given as a dictionary), rather than just its data.

Click, tap and hover

Let's now display the data generated whenever you click or hover over a node or an edge. Simply replace the previous layout and callbacks by this:

using Dash, DashCytoscape
using JSON

app = dash()

styles = Dict(
    "pre" =>  Dict(
        "border" =>  "thin lightgrey solid",
        "overflowX" =>  "scroll"
    )
)

nodes = [
    Dict(
        "data" =>  Dict("id" =>  short, "label" =>  label),
        "position" =>  Dict("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 = [
    Dict("data" =>  Dict("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")
    )
]

default_stylesheet = [
    Dict(
        "selector" =>  "node",
        "style" =>  Dict(
            "background-color" =>  "#BFD7B5",
            "label" =>  "data(label)"
        )
    )
]

app.layout = html_div([
    cyto_cytoscape(
        id="cytoscape-event-callbacks-2",
        layout=Dict("name" =>  "preset"),
        elements=vcat(edges,nodes),
        stylesheet=default_stylesheet,
        style=Dict("width" =>  "100%", "height" =>  "450px")
    ),
    html_p(id="cytoscape-tapNodeData-output"),
    html_p(id="cytoscape-tapEdgeData-output"),
    html_p(id="cytoscape-mouseoverNodeData-output"),
    html_p(id="cytoscape-mouseoverEdgeData-output")
])

callback!(app,
    Output("cytoscape-tapNodeData-output", "children"),
    Input("cytoscape-event-callbacks-2", "tapNodeData")
) do data
    if !(data isa Nothing)
        return ("You recently clicked/tapped the 
        city =>  $(data[:label])")
    end
end

callback!(app,
    Output("cytoscape-tapEdgeData-output", "children"),
    Input("cytoscape-event-callbacks-2", "tapEdgeData")
) do data
    if !(data isa Nothing)
        return "You recently clicked/tapped the edge 
        between $(uppercase(data[:source])) 
        and $(uppercase(data[:target]))"
    end
end

callback!(app,
    Output("cytoscape-mouseoverNodeData-output", "children"),
    Input("cytoscape-event-callbacks-2", "mouseoverNodeData")
) do data
    if !(data isa Nothing)
        return "You recently hovered over 
        the city =>  $(data[:label])"
    end
end

callback!(app,
    Output("cytoscape-mouseoverEdgeData-output", "children"),
    Input("cytoscape-event-callbacks-2", "mouseoverEdgeData")
) do data
    if !(data isa Nothing)
        return "You recently hovered over the edge 
        between $(uppercase(data[:source])) 
        and $(uppercase(data[:target]))"
    end
end

run_server(app, "0.0.0.0", debug=true)

Selecting multiple elements

Additionally, you can also display all the data currently selected, either through a box selection (Shift+Click and drag) or by individually selecting multiple elements while holding Shift:

using Dash, DashCytoscape

app = dash()

styles = Dict(
    "pre" =>  Dict(
        "border" =>  "thin lightgrey solid",
        "overflowX" =>  "scroll"
    )
)

nodes = [
    Dict(
        "data" =>  Dict("id" =>  short, "label" =>  label),
        "position" =>  Dict("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 = [
    Dict("data" =>  Dict("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")
    )
]

default_stylesheet = [
    Dict(
        "selector" =>  "node",
        "style" =>  Dict(
            "background-color" =>  "#BFD7B5",
            "label" =>  "data(label)"
        )
    )
]

app.layout = html_div([
    cyto_cytoscape(
        id="cytoscape-event-callbacks-3",
        layout=Dict("name" => "preset"),
        elements=vcat(edges,nodes),
        stylesheet=default_stylesheet,
        style=Dict("width" => "100%", "height" => "450px")
    ),
    dcc_markdown(id="cytoscape-selectedNodeData-markdown")
])

callback!(app,
    Output("cytoscape-selectedNodeData-markdown", "children"),
    Input("cytoscape-event-callbacks-3", "selectedNodeData")
) do data_list
    if (data_list isa Nothing)
        return
    end

    cities_list = [data[:label] for data in data_list]
    return """
        You selected the following cities:\n $(join(cities_list,"\n"))
        """
end

run_server(app, "0.0.0.0", debug=true)

No city selected.

Advanced usage of callbacks

Those event callbacks enable more advanced interactions between components. In fact, you can even use them to update other Cytoscape arguments. The usage-stylesheet.py example (hosted on the dash-cytoscape Github repo) lets you click to change the color of a node to purple, its targeted nodes to red, and its incoming nodes to blue. All of this is done using a single callback function, which takes as input the tapNode prop of the Cytoscape component along with a few dropdowns, and outputs to the stylesheet prop. You can try out this interactive stylesheet demo hosted on Dash Enterprise.

Additionally, usage-elements.py lets you progressively expand your graph by using tapNodeData as the input and elements as the output.

The app initially pre-loads the entire dataset, but only loads the graph with a single node. It then constructs four dictionaries that maps every single node ID to its following nodes, following edges, followers nodes, followers edges.

Then, it lets you expand the incoming or the outgoing neighbors by clicking the node you want to expand. This is done through a callback that retrieves the followers (outgoing) or following (incoming) from the dictionaries, and add the to the elements. Click here for the online demo.

To see more examples of events, check out the event callbacks demo (the source file is available as usage-events.py on the project repo) and the Cytoscape references.