Cytoscape Event Callbacks

In part 4, we showed how to update Cytoscape with
other components by assigning callbacks that output to 'elements', 'stylesheet', 'layout'.

It is also possible to use properties
of Cytoscape as an input to callbacks, which then update other
components or Cytoscape itself. These properties are updated when the user interact with elements in the app,
so we refer to the fired callbacks as event callbacks.

Some key props include
tapNode, which returns a complete description of the node object when
the user clicks or taps on a node, and mouseoverEdgeData, which returns only
the data dictionary of the edge that was most recently hovered by the user.
Find the complete list in the Dash Cytoscape reference.

The following app demonstrates how to capture user interaction events.

import json

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

app = Dash()

# Object declaration
basic_elements = [
    {"data": {"id": "one", "label": "Node 1"}, "renderedPosition": {"x": 50, "y": 50}},
    {
        "data": {"id": "two", "label": "Node 2"},
        "renderedPosition": {"x": 200, "y": 200},
    },
    {
        "data": {"id": "three", "label": "Node 3"},
        "renderedPosition": {"x": 100, "y": 150},
    },
    {
        "data": {"id": "four", "label": "Node 4"},
        "renderedPosition": {"x": 400, "y": 50},
    },
    {
        "data": {"id": "five", "label": "Node 5"},
        "renderedPosition": {"x": 250, "y": 100},
    },
    {
        "data": {"id": "six", "label": "Node 6", "parent": "three"},
        "renderedPosition": {"x": 150, "y": 150},
    },
    {
        "data": {
            "id": "one-two",
            "source": "one",
            "target": "two",
            "label": "Edge from Node 1 to Node 2",
        }
    },
    {
        "data": {
            "id": "one-five",
            "source": "one",
            "target": "five",
            "label": "Edge from Node 1 to Node 5",
        }
    },
    {
        "data": {
            "id": "two-four",
            "source": "two",
            "target": "four",
            "label": "Edge from Node 2 to Node 4",
        }
    },
    {
        "data": {
            "id": "three-five",
            "source": "three",
            "target": "five",
            "label": "Edge from Node 3 to Node 5",
        }
    },
    {
        "data": {
            "id": "three-two",
            "source": "three",
            "target": "two",
            "label": "Edge from Node 3 to Node 2",
        }
    },
    {
        "data": {
            "id": "four-four",
            "source": "four",
            "target": "four",
            "label": "Edge from Node 4 to Node 4",
        }
    },
    {
        "data": {
            "id": "four-six",
            "source": "four",
            "target": "six",
            "label": "Edge from Node 4 to Node 6",
        }
    },
    {
        "data": {
            "id": "five-one",
            "source": "five",
            "target": "one",
            "label": "Edge from Node 5 to Node 1",
        }
    },
]
context_menu = [
    {
        "id": "add-node",
        "label": "Add Node (JS)",
        "tooltipText": "Add Node",
        "availableOn": ["canvas"],
        "onClick": "add_node",
    },
    {
        "id": "remove",
        "label": "Remove (JS)",
        "tooltipText": "Remove",
        "availableOn": ["node", "edge"],
        "onClick": "remove",
    },
    {
        "id": "add-edge",
        "label": "Add Edge (JS)",
        "tooltipText": "add edge",
        "availableOn": ["node"],
        "onClick": "add_edge",
    },
]

styles = {
    "json-output": {
        "overflow-y": "scroll",
        "height": "calc(50% - 25px)",
        "border": "thin lightgrey solid",
    },
    "tab": {"height": "calc(98vh - 115px)"},
}

app.layout = html.Div(
    [
        html.Div(
            className="twelve columns",
            children=[
                cyto.Cytoscape(
                    id="cytoscape-usage-events",
                    elements=basic_elements,
                    contextMenu=context_menu,
                    layout={"name": "preset"},
                    style={"height": "500px", "width": "500px"},
                    clearOnUnhover=True,
                )
            ],
        ),
        html.Div(
            className="twelve columns",
            children=[
                dcc.Tabs(
                    id="tabs-cytoscape-events",
                    children=[
                        dcc.Tab(
                            label="Tap Objects",
                            children=[
                                html.Div(
                                    style=styles["tab"],
                                    children=[
                                        html.P("Node Object JSON:"),
                                        html.Pre(
                                            id="usage-elements-events-tap-node-json-output",
                                            style=styles["json-output"],
                                        ),
                                        html.P("Edge Object JSON:"),
                                        html.Pre(
                                            id="usage-elements-events-tap-edge-json-output",
                                            style=styles["json-output"],
                                        ),
                                    ],
                                )
                            ],
                        ),
                        dcc.Tab(
                            label="Tap Data",
                            children=[
                                html.Div(
                                    style=styles["tab"],
                                    children=[
                                        html.P("Node Data JSON:"),
                                        html.Pre(
                                            id="usage-elements-events-tap-node-data-json-output",
                                            style=styles["json-output"],
                                        ),
                                        html.P("Edge Data JSON:"),
                                        html.Pre(
                                            id="usage-elements-events-tap-edge-data-json-output",
                                            style=styles["json-output"],
                                        ),
                                    ],
                                )
                            ],
                        ),
                        dcc.Tab(
                            label="Mouseover Data",
                            children=[
                                html.Div(
                                    style=styles["tab"],
                                    children=[
                                        html.P("Node Data JSON:"),
                                        html.Pre(
                                            id="usage-elements-events-mouseover-node-data-json-output",
                                            style=styles["json-output"],
                                        ),
                                        html.P("Edge Data JSON:"),
                                        html.Pre(
                                            id="usage-elements-events-mouseover-edge-data-json-output",
                                            style=styles["json-output"],
                                        ),
                                    ],
                                )
                            ],
                        ),
                        dcc.Tab(
                            label="Selected Data",
                            children=[
                                html.Div(
                                    style=styles["tab"],
                                    children=[
                                        html.P("Node Data JSON:"),
                                        html.Pre(
                                            id="usage-elements-events-selected-node-data-json-output",
                                            style=styles["json-output"],
                                        ),
                                        html.P("Edge Data JSON:"),
                                        html.Pre(
                                            id="usage-elements-events-selected-edge-data-json-output",
                                            style=styles["json-output"],
                                        ),
                                    ],
                                )
                            ],
                        ),
                        dcc.Tab(
                            label="Drag Data",
                            children=[
                                html.Div(
                                    style=styles["tab"],
                                    children=[
                                        html.P("Elements Data JSON:"),
                                        html.Pre(
                                            id="usage-elements-events-elements-data-json-output",
                                            style=styles["json-output"],
                                        ),
                                    ],
                                )
                            ],
                        ),
                    ],
                ),
            ],
        ),
        html.Div(id="placeholder-cytoscape-events"),
    ]
)


@callback(
    Output("usage-elements-events-tap-node-json-output", "children"),
    Input("cytoscape-usage-events", "tapNode"),
)
def displayTapNode(data):
    return json.dumps(data, indent=2)


@callback(
    Output("usage-elements-events-tap-edge-json-output", "children"),
    Input("cytoscape-usage-events", "tapEdge"),
)
def displayTapEdge(data):
    return json.dumps(data, indent=2)


@callback(
    Output("usage-elements-events-tap-node-data-json-output", "children"),
    Input("cytoscape-usage-events", "tapNodeData"),
)
def displayTapNodeData(data):
    return json.dumps(data, indent=2)


@callback(
    Output("usage-elements-events-tap-edge-data-json-output", "children"),
    Input("cytoscape-usage-events", "tapEdgeData"),
)
def displayTapEdgeData(data):
    return json.dumps(data, indent=2)


@callback(
    Output("usage-elements-events-mouseover-node-data-json-output", "children"),
    Input("cytoscape-usage-events", "mouseoverNodeData"),
)
def displayMouseoverNodeData(data):
    return json.dumps(data, indent=2)


@callback(
    Output("usage-elements-events-mouseover-edge-data-json-output", "children"),
    Input("cytoscape-usage-events", "mouseoverEdgeData"),
)
def displayMouseoverEdgeData(data):
    return json.dumps(data, indent=2)


@callback(
    Output("usage-elements-events-selected-node-data-json-output", "children"),
    Input("cytoscape-usage-events", "selectedNodeData"),
)
def displaySelectedNodeData(data):
    return json.dumps(data, indent=2)


@callback(
    Output("usage-elements-events-selected-edge-data-json-output", "children"),
    Input("cytoscape-usage-events", "selectedEdgeData"),
)
def displaySelectedEdgeData(data):
    return json.dumps(data, indent=2)


@callback(
    Output("usage-elements-events-elements-data-json-output", "children"),
    Input("cytoscape-usage-events", "elements"),
)
def displayElementsData(data):
    return json.dumps(data, indent=2)


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

Simple Callback Construction

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

from dash import Dash, html
import dash_cytoscape as cyto

app = Dash()

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')
    )
]

default_stylesheet = [
    {
        'selector': 'node',
        'style': {
            'background-color': '#BFD7B5',
            'label': 'data(label)'
        }
    },
    {
        'selector': 'edge',
        'style': {
            'line-color': '#A3C4BC'
        }
    }
]

app.layout = html.Div([
    cyto.Cytoscape(
        id='cytoscape-events',
        layout={'name': 'preset'},
        elements=edges + nodes,
        stylesheet=default_stylesheet,
        style={'width': '100%', 'height': '450px'}
    )
])

if __name__ == '__main__':
    app.run(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:

import json

from dash import Dash, html, Input, Output, callback
import dash_cytoscape as cyto

app = Dash()

styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}


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')
    )
]


default_stylesheet = [
    {
        'selector': 'node',
        'style': {
            'background-color': '#BFD7B5',
            'label': 'data(label)'
        }
    }
]


app.layout = html.Div([
    cyto.Cytoscape(
        id='cytoscape-event-callbacks-1',
        layout={'name': 'preset'},
        elements=edges+nodes,
        stylesheet=default_stylesheet,
        style={'width': '100%', 'height': '450px'}
    ),
    html.Pre(id='cytoscape-tapNodeData-json', style=styles['pre'])
])


@callback(Output('cytoscape-tapNodeData-json', 'children'),
              Input('cytoscape-event-callbacks-1', 'tapNodeData'))
def displayTapNodeData(data):
    return json.dumps(data, indent=2)


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

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:

from dash import Dash, html, Input, Output, callback
import dash_cytoscape as cyto

app = Dash()

styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}


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')
    )
]


default_stylesheet = [
    {
        'selector': 'node',
        'style': {
            'background-color': '#BFD7B5',
            'label': 'data(label)'
        }
    }
]


app.layout = html.Div([
    cyto.Cytoscape(
        id='cytoscape-event-callbacks-2',
        layout={'name': 'preset'},
        elements=edges+nodes,
        stylesheet=default_stylesheet,
        style={'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(Output('cytoscape-tapNodeData-output', 'children'),
              Input('cytoscape-event-callbacks-2', 'tapNodeData'))
def displayTapNodeData(data):
    if data:
        return "You recently clicked/tapped the city: " + data['label']


@callback(Output('cytoscape-tapEdgeData-output', 'children'),
              Input('cytoscape-event-callbacks-2', 'tapEdgeData'))
def displayTapEdgeData(data):
    if data:
        return "You recently clicked/tapped the edge between " + \
               data['source'].upper() + " and " + data['target'].upper()


@callback(Output('cytoscape-mouseoverNodeData-output', 'children'),
              Input('cytoscape-event-callbacks-2', 'mouseoverNodeData'))
def displayTapNodeData(data):
    if data:
        return "You recently hovered over the city: " + data['label']


@callback(Output('cytoscape-mouseoverEdgeData-output', 'children'),
              Input('cytoscape-event-callbacks-2', 'mouseoverEdgeData'))
def displayTapEdgeData(data):
    if data:
        return "You recently hovered over the edge between " + \
               data['source'].upper() + " and " + data['target'].upper()


if __name__ == '__main__':
    app.run(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:

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

app = Dash()

styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}


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')
    )
]


default_stylesheet = [
    {
        'selector': 'node',
        'style': {
            'background-color': '#BFD7B5',
            'label': 'data(label)'
        }
    }
]


app.layout = html.Div([
    cyto.Cytoscape(
        id='cytoscape-event-callbacks-3',
        layout={'name': 'preset'},
        elements=edges+nodes,
        stylesheet=default_stylesheet,
        style={'width': '100%', 'height': '450px'}
    ),
    dcc.Markdown(id='cytoscape-selectedNodeData-markdown')
])


@callback(Output('cytoscape-selectedNodeData-markdown', 'children'),
              Input('cytoscape-event-callbacks-3', 'selectedNodeData'))
def displaySelectedNodeData(data_list):
    if data_list is None:
        return "No city selected."

    cities_list = [data['label'] for data in data_list]
    return "You selected the following cities: " + "\n* ".join(cities_list)


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

None

Advanced Usage of Callbacks

Event callbacks enable more advanced interactions between components.
In fact, you can even use them to update other Cytoscape arguments. The
following example 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.

import json
from urllib.request import urlopen

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

app = Dash()

# ###################### DATA PREPROCESSING ######################
# Load data

network_data = urlopen("https://raw.githubusercontent.com/plotly/dash-cytoscape/master/demos/data/sample_network.txt").read().decode("utf-8").split("\n")

# We select the first 150 edges and associated nodes for an easier visualization
edges = network_data[:150]
nodes = set()

cy_edges = []
cy_nodes = []

for network_edge in edges:
    source, target = network_edge.split(" ")

    if source not in nodes:
        nodes.add(source)
        cy_nodes.append({"data": {"id": source, "label": "User #" + source[-5:]}})
    if target not in nodes:
        nodes.add(target)
        cy_nodes.append({"data": {"id": target, "label": "User #" + target[-5:]}})

    cy_edges.append({"data": {"source": source, "target": target}})

styles = {
    "json-output": {
        "overflow-y": "scroll",
        "height": "calc(50% - 25px)",
        "border": "thin lightgrey solid",
    },
    "tab": {
        "height": "calc(98vh - 105px)",
        "overflow":"visible"
        },
}

app.layout = html.Div(
    [
        html.Div(
            className="eight columns",
            children=[
                cyto.Cytoscape(
                    id="cytoscape-stylesheet",
                    elements=cy_edges + cy_nodes,
                    style={"height": "95vh", "width": "100%"},
                )
            ],
        ),
        html.Div(
            className="four columns",
            children=[
                dcc.Tabs(
                    id="tabs-stylesheet",
                    children=[
                        dcc.Tab(
                            label="Control Panel",
                            children=html.Div([
                                html.Div(
                                    [
                                        html.Label("Layout"),
                                        dcc.Dropdown(
                                            id="dropdown-layout",
                                            options=[
                                                "random",
                                                "grid",
                                                "circle",
                                                "concentric",
                                                "breadthfirst",
                                                "cose",
                                            ],
                                            value="grid",
                                            clearable=False,
                                        ),
                                    ]
                                ),
                                html.Div(
                                    [
                                        html.Label("Node Shape"),
                                        dcc.Dropdown(
                                            id="dropdown-node-shape",
                                            value="ellipse",
                                            clearable=False,
                                            options=[
                                                "ellipse",
                                                "triangle",
                                                "rectangle",
                                                "diamond",
                                                "pentagon",
                                                "hexagon",
                                                "heptagon",
                                                "octagon",
                                                "star",
                                                "polygon",
                                            ],
                                        ),
                                    ]
                                ),
                                html.Div(
                                    [
                                        html.Label("Followers Color"),
                                        dcc.Input(
                                            id="input-follower-color",
                                            type="text",
                                            value="#0074D9",
                                        ),
                                    ]
                                ),
                                html.Div(
                                    [
                                        html.Label("Following Color"),
                                        dcc.Input(
                                            id="input-following-color",
                                            type="text",
                                            value="#FF4136",
                                        ),
                                    ]
                                ),
                            ],
                            style=styles["tab"]
                            ),
                        ),
                        dcc.Tab(
                            label="JSON",
                            children=[
                                html.Div(
                                    style=styles["tab"],
                                    children=[
                                        html.P("Node Object JSON:"),
                                        html.Pre(
                                            id="tap-node-json-output",
                                            style=styles["json-output"],
                                        ),
                                        html.P("Edge Object JSON:"),
                                        html.Pre(
                                            id="tap-edge-json-output",
                                            style=styles["json-output"],
                                        ),
                                    ],
                                )
                            ],
                        ),
                    ],
                ),
            ],
        ),
    ]
)


@callback(
    Output("tap-node-json-output", "children"),
    Input("cytoscape-stylesheet", "tapNode"),
)
def display_tap_node(data):
    return json.dumps(data, indent=2)


@callback(
    Output("tap-edge-json-output", "children"),
    Input("cytoscape-stylesheet", "tapEdge"),
)
def display_tap_edge(data):
    return json.dumps(data, indent=2)


@callback(
    Output("cytoscape-stylesheet", "layout"),
    Input("dropdown-layout", "value"),
)
def update_cytoscape_layout(layout):
    return {"name": layout}


@callback(
    Output("cytoscape-stylesheet", "stylesheet"),
    [
        Input("cytoscape-stylesheet", "tapNode"),
        Input("input-follower-color", "value"),
        Input("input-following-color", "value"),
        Input("dropdown-node-shape", "value"),
        State("cytoscape-stylesheet", "stylesheet")
    ],
    prevent_initial_call=True
)
def generate_stylesheet(node, follower_color, following_color, node_shape, stylesheet):

    stylesheet = [
        {
            "selector": "node", 
            "style": {
                "opacity": 0.3 if node else 0.65, 
                "shape": node_shape,
                }
            },
        {
            "selector": "edge",
            "style": {
                "opacity": 0.2 if node else 0.65,
                "curve-style": "bezier",
            },
        },
    ]

    if node: 
        stylesheet += [
            {
                "selector": f'node[id = "{node["data"]["id"]}"]',
                "style": {
                    "background-color": "#B10DC9",
                    "border-color": "purple",
                    "border-width": 2,
                    "border-opacity": 1,
                    "opacity": 1,
                    "label": "data(label)",
                    "color": "#B10DC9",
                    "text-opacity": 1,
                    "font-size": 12,
                    "z-index": 9999,
                },
            },
        ]

        for edge in node["edgesData"]:
            if edge["source"] == node["data"]["id"]:
                stylesheet.append(
                    {
                        "selector": f'node[id = "{edge["target"]}"]',
                        "style": {"background-color": following_color, "opacity": 0.9},
                    }
                )
                stylesheet.append(
                    {
                        "selector": f'edge[id= "{edge["id"]}"]',
                        "style": {
                            "mid-target-arrow-color": following_color,
                            "mid-target-arrow-shape": "vee",
                            "line-color": following_color,
                            "opacity": 0.9,
                            "z-index": 5000,
                        },
                    }
                )

            if edge["target"] == node["data"]["id"]:
                stylesheet.append(
                    {
                        "selector": f'node[id = "{edge["source"]}"]',
                        "style": {
                            "background-color": follower_color,
                            "opacity": 0.9,
                            "z-index": 9999,
                        },
                    }
                )
                stylesheet.append(
                    {
                        "selector": f'edge[id= "{edge["id"]}"]',
                        "style": {
                            "mid-target-arrow-color": follower_color,
                            "mid-target-arrow-shape": "vee",
                            "line-color": follower_color,
                            "opacity": 1,
                            "z-index": 5000,
                        },
                    }
                )

    return stylesheet


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

The next example 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, and
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 adds them to the elements.

import json
from urllib.request import urlopen

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

app = Dash()

# ###################### DATA PREPROCESSING ######################
# Load data

network_data = urlopen("https://raw.githubusercontent.com/plotly/dash-cytoscape/master/demos/data/sample_network.txt").read().decode("utf-8").split("\n")

# We select the first 750 edges and associated nodes for an easier visualization
edges = network_data[:750]
nodes = set()

following_node_di = {}  # user id -> list of users they are following
following_edges_di = {}  # user id -> list of cy edges starting from user id

followers_node_di = {}  # user id -> list of followers (cy_node format)
followers_edges_di = {}  # user id -> list of cy edges ending at user id

cy_edges = []
cy_nodes = []

for edge in edges:
    if " " not in edge:
        continue

    source, target = edge.split(" ")

    cy_edge = {"data": {"id": source + target, "source": source, "target": target}}
    cy_target = {"data": {"id": target, "label": "User #" + str(target[-5:])}}
    cy_source = {"data": {"id": source, "label": "User #" + str(source[-5:])}}

    if source not in nodes:
        nodes.add(source)
        cy_nodes.append(cy_source)
    if target not in nodes:
        nodes.add(target)
        cy_nodes.append(cy_target)

    # Process dictionary of following
    if not following_node_di.get(source):
        following_node_di[source] = []
    if not following_edges_di.get(source):
        following_edges_di[source] = []

    following_node_di[source].append(cy_target)
    following_edges_di[source].append(cy_edge)

    # Process dictionary of followers
    if not followers_node_di.get(target):
        followers_node_di[target] = []
    if not followers_edges_di.get(target):
        followers_edges_di[target] = []

    followers_node_di[target].append(cy_source)
    followers_edges_di[target].append(cy_edge)

genesis_node = cy_nodes[0]
genesis_node["classes"] = "genesis"
default_elements = [genesis_node]

default_stylesheet = [
    {"selector": "node", "style": {"opacity": 0.65, "z-index": 9999}},
    {
        "selector": "edge",
        "style": {"curve-style": "bezier", "opacity": 0.45, "z-index": 5000},
    },
    {"selector": ".followerNode", "style": {"background-color": "#0074D9"}},
    {
        "selector": ".followerEdge",
        "style": {
            "mid-target-arrow-color": "blue",
            "mid-target-arrow-shape": "vee",
            "line-color": "#0074D9",
        },
    },
    {"selector": ".followingNode", "style": {"background-color": "#FF4136"}},
    {
        "selector": ".followingEdge",
        "style": {
            "mid-target-arrow-color": "red",
            "mid-target-arrow-shape": "vee",
            "line-color": "#FF4136",
        },
    },
    {
        "selector": ".genesis",
        "style": {
            "background-color": "#B10DC9",
            "border-width": 2,
            "border-color": "purple",
            "border-opacity": 1,
            "opacity": 1,
            "label": "data(label)",
            "color": "#B10DC9",
            "text-opacity": 1,
            "font-size": 12,
            "z-index": 9999,
        },
    },
    {
        "selector": ":selected",
        "style": {
            "border-width": 2,
            "border-color": "black",
            "border-opacity": 1,
            "opacity": 1,
            "label": "data(label)",
            "color": "black",
            "font-size": 12,
            "z-index": 9999,
        },
    },
]

# ################################# APP LAYOUT ################################
styles = {
    "json-output": {
        "overflow-y": "scroll",
        "height": "calc(50% - 25px)",
        "border": "thin lightgrey solid",
    },
    "tab": {"height": "calc(98vh - 80px)"},
}

app.layout = html.Div(
    [
        html.Div(
            className="eight columns",
            children=[
                cyto.Cytoscape(
                    id="cytoscape-elements-extra",
                    elements=default_elements,
                    stylesheet=default_stylesheet,
                    style={"height": "95vh", "width": "100%"},
                )
            ],
        ),
        html.Div(
            className="four columns",
            children=[
                dcc.Tabs(
                    id="tabs-elements-extra",
                    children=[
                        dcc.Tab(
                            label="Control Panel",
                            children=[
                                html.Div(
                                    [
                                        html.Label("Layout"),
                                        dcc.Dropdown(
                                            id="dropdown-layout-elements-extra",
                                            options=[
                                                "random",
                                                "grid",
                                                "circle",
                                                "concentric",
                                                "breadthfirst",
                                                "cose",
                                                "cose-bilkent",
                                                "dagre",
                                                "cola",
                                                "klay",
                                                "spread",
                                                "euler",
                                            ],
                                            value="grid",
                                            clearable=False,
                                        ),
                                    ]
                                ),
                                html.Div(
                                    [
                                        html.Label("Expand"),
                                        dcc.RadioItems(
                                            id="radio-expand-elements-extra",
                                            options=["followers", "following"],
                                            value="followers",
                                        ),
                                    ]
                                ),
                            ],
                        ),
                        dcc.Tab(
                            label="JSON",
                            children=[
                                html.Div(
                                    style=styles["tab"],
                                    children=[
                                        html.P("Node Object JSON:"),
                                        html.Pre(
                                            id="elements-extra-tap-node-json-output",
                                            style=styles["json-output"],
                                        ),
                                        html.P("Edge Object JSON:"),
                                        html.Pre(
                                            id="elements-extra-tap-edge-json-output",
                                            style=styles["json-output"],
                                        ),
                                    ],
                                )
                            ],
                        ),
                    ],
                ),
            ],
        ),
    ]
)


# ############################## CALLBACKS ####################################
@callback(
    Output("elements-extra-tap-node-json-output", "children"),
    Input("cytoscape-elements-extra", "tapNode"),
)
def display_tap_node(data):
    return json.dumps(data, indent=2)


@callback(
    Output("elements-extra-tap-edge-json-output", "children"),
    Input("cytoscape-elements-extra", "tapEdge"),
)
def display_tap_edge(data):
    return json.dumps(data, indent=2)


@callback(
    Output("cytoscape-elements-extra", "layout"),
    Input("dropdown-layout-elements-extra", "value"),
)
def update_cytoscape_layout(layout):
    return {"name": layout}


@callback(
    Output("cytoscape-elements-extra", "elements"),
    Input("cytoscape-elements-extra", "tapNodeData"),
    State("cytoscape-elements-extra", "elements"),
    State("radio-expand-elements-extra", "value"),
)
def generate_elements(nodeData, elements, expansion_mode):
    if not nodeData:
        return default_elements

    # If the node has already been expanded, we don't expand it again
    if nodeData.get("expanded"):
        return elements

    # This retrieves the currently selected element, and tag it as expanded
    for element in elements:
        if nodeData["id"] == element.get("data").get("id"):
            element["data"]["expanded"] = True
            break

    if expansion_mode == "followers":
        followers_nodes = followers_node_di.get(nodeData["id"])
        followers_edges = followers_edges_di.get(nodeData["id"])

        if followers_nodes:
            for node in followers_nodes:
                node["classes"] = "followerNode"
            elements.extend(followers_nodes)

        if followers_edges:
            for follower_edge in followers_edges:
                follower_edge["classes"] = "followerEdge"
            elements.extend(followers_edges)

    elif expansion_mode == "following":
        following_nodes = following_node_di.get(nodeData["id"])
        following_edges = following_edges_di.get(nodeData["id"])

        if following_nodes:
            for node in following_nodes:
                if node["data"]["id"] != genesis_node["data"]["id"]:
                    node["classes"] = "followingNode"
                    elements.append(node)

        if following_edges:
            for follower_edge in following_edges:
                follower_edge["classes"] = "followingEdge"
            elements.extend(following_edges)

    return elements


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