Dash Cytoscape

Since 2022 I've been writing full-time for Coders Lab. It's a bootcamp based in Poland with a mix of online and in-person open courses aimed at reskilling into junior IT jobs and B2B tailored upskilling courses for developers. For me that means juggling projects as diverse as front-end, back-end, data analysis and testing with languages ranging from SQL, through JavaScript and Python, to Java and C# (.NET). On the other hand I'm mostly dealing with markdown (yay!), HTML, YAML and JSON (mostly as serialized data or Jupyter Notebooks).

Introduction

Dash Cytoscape is a Dash component that enables easy creation of high-quality interactive graphs. By using the Cytoscape.js library, Dash Cytoscape will create graph objects that we can easily integrate into the Dash application and manipulate their appearance and content through the use of callbacks.

Installation

To use the library in Colab we need to install it first, just like the Dash library:

!pip install dash
!pip install dash-cytoscape

Then, we can use it to build graphs, like we previously used elements of the dash.dash_table library. Let's take a look at the first examples to better understand how it works.

Preparing the application

First, we need to import the libraries. In the simplest variant, apart form the dash library and its dash.html, we also need to import threading, dash_cytoscape, and the google.colab.output class:

import dash
import threading
import dash_cytoscape as cyto
from dash import html
# from dash.dependencies import Input, Output
from google.colab import output

Next, we create an instance of dash.Dash() and its layout() as a single container html.Div().

Inside this container we create a single instance of dash_cytoscape.Cytoscape:

app = dash.Dash()

app.layout = html.Div([
    cyto.Cytoscape(
            id='cytoscape-simple-graph',
            layout={'name': 'random'},
            style={'width': '100%', 'height': '600px'},
            elements=[
                {'data': {'id': 'first', 'label': 'Alice'},},
                {'data': {'id': 'second', 'label': 'Bob'},},
                {'data': {'id': 'third', 'label': 'Carol'},},
                {'data': {'id': 'fourth', 'label': 'Dan'},},
                {'data': {'source': 'first', 'target': 'second'}},
                {'data': {'source': 'second', 'target': 'fourth'}},
                {'data': {'source': 'third', 'target': 'first'}},
                {'data': {'source': 'fourth', 'target': 'third'}},
            ]
        )
    ])

You can display the application created like this in the same way as before:

thread = threading.Thread(target=app.run_server)
thread.start()

output.serve_kernel_port_as_iframe(port=8050, height='600')

To create the graph, in cyto.Cytoscape we indicated:

  • id as a unique identifier of the object; as with other elements of the application, it allows us to uniquely refer to the created graph in other parts of the application,

  • layout, which is the way the graph is displayed; for simplicity, we have assumed that the notes are to be in random locations - we will learn other layouts soon,

  • style to set the style of the graph display - we want the entire available space to be used,

  • elements as a list of graph elements.

When using Dash Cytoscape, each graph element corresponds to a dictionary, which contains information about the element and its properties.

In our example, each node of the graph in the elements list corresponds to a dictionary; the elements of the dictionary are the id of the given node and its label.

In the case of edges, the elements of the dictionary are the node where the edge starts (source), and the one where it leads (target). These are the keys required when we add an edge to the graph.

Interactivity

Note that our graph created with Dash Cytoscape is interactive by default, We can drag its position, individual nodes, and zoom in or zoom out on the graph.

In case the graph has only a few nodes, we usually have no need to use these functionalities. However, if our graph contains many nodes and a diverse arrangement of edges connecting them, interactivity of the graph will make it much easier for us to traverse it.

For example, let's imagine that we would like to determine the possible way of getting from Bronowice Małe to Łagiewniki in Cracow, using city trams or buses. We are interested in any stop that is no more than 700 meters from the Łagiewniki stop as the start of the route and any stop that is no more than 700 meters from the Bronowice Małe stop.

In Cracow we have more than 20 tramlines. There are almost 200 bus lines. On top of that, there are nearly 1500 stops.

In order to determine all the possibilities of getting from point A to point B in the described way, we can use a graph, where the nodes will be the stops, and the city transit lines will determine the edges connecting the individual stops. We can indicate distances between stops as edge weights. A well-designed application will allow us to easily indicate the stops that meet the distance condition (for example, by highlighting them in color after pointing to the stop with a mouse cursor). If we select the layout of the graph well, we can easily search it for the paths we need. In such a situation, it is easy to imagine how useful zooming and moving the graph is for task like this one.

Let's get better acquainted with the elements of Dash Cytoscape that will help us create such applications.

Graph layout

In the example we built, we indicated that the nodes are to be placed in random locations. Keeping track of possible connections between Bronowice Małe and Łagiewniki would be much more difficult, if stops which are actually close to each other were drawn in random, distant parts of the graph.

Let's look at other possibilities of positioning the elements of the graph.

If we want to indicate the positions where the nodes should be located, we should:

  • change the name value of the layout parameter dictionary to preset,

  • extend the definitions of the graph's nodes with their coordinates.

Let's modify the above simple example to see how such an application works in practice.

app = dash.Dash()

app.layout = html.Div([
    cyto.Cytoscape(
            id='cytoscape-simple-graph',
            layout={'name': 'preset'},
            style={'width': '100%', 'height': '600px'},
            elements=[
                {
                    'data': {'id': 'first', 'label': 'Alice'},
                    'position': {'x': 20, 'y': 70}
                 },
                {
                    'data': {'id': 'second', 'label': 'Bob'},
                    'position': {'x': 170, 'y': 240}
                 },
                {
                    'data': {'id': 'third', 'label': 'Carol'},
                    'position': {'x': 330, 'y': 490}
                 },
                {
                    'data': {'id': 'fourth', 'label': 'Dan'},
                    'position': {'x': 540, 'y': 160}
                 },
                {'data': {'source': 'first', 'target': 'second'}},
                {'data': {'source': 'second', 'target': 'fourth'}},
                {'data': {'source': 'third', 'target': 'first'}},
                {'data': {'source': 'fourth', 'target': 'third'}},
            ]
        )
    ])

Alternatively, we can use several other settings where the positions of the nodes are calculated automatically. We just need to change the value of the name key in the layout parameter to:

  • grid, so that the nodes are arranged on a uniform coordinate grid,

  • circle, to have the nodes aligned on a circle,

  • concentric, to have the nodes aligned in concentric circles,

  • breadthfirst, to have the nodes aligned as a hierarchy; this is especially important if you need to visualize, e.g., decision trees or random forests.

To learn more possible settings we can use the available documentation.

Interaction control

In the case of traversing a graph with stops, we could highlight the stops that are out of service, preventing the application user from selecting them.

Below we discuss some settings that we can indicate for the nodes of a graph, determining how the interaction with the user takes place.

Let's run the following application in Colab and analyze the setting of each flag.

app = dash.Dash()

app.layout = html.Div([
    cyto.Cytoscape(
            id='cytoscape-simple-graph',
            layout={'name': 'circle'},
            style={'width': '100%', 'height': '600px'},
            elements=[
                {
                    'data': {'id': 'first', 'label': 'Alice'},
                    'locked': True
                 },
                {
                    'data': {'id': 'second', 'label': 'Bob'},
                    'selected': True
                 },
                {
                    'data': {'id': 'third', 'label': 'Carol'},
                    'selectable': False
                 },
                {
                    'data': {'id': 'fourth', 'label': 'Dan'},
                    'grabbable': False
                 },
                {'data': {'source': 'first', 'target': 'second'}},
                {'data': {'source': 'second', 'target': 'fourth'}},
                {'data': {'source': 'third', 'target': 'first'}},
                {'data': {'source': 'fourth', 'target': 'third'}},
            ]
        )
    ])

Each of the used flags (locked, selected, selectable, grabbable) takes only True or False values:

  • locked - if the position of the node is to be locked, i.e. it cannot be moved either as a result of the program execution or by user interaction,

  • selected - indicates that the given node is currently selected,

  • selectable - determines whether the user can select a given node,

  • grabbable - determines whether the user can drag the node to another location.

Displaying labels of edges

Dash Cytoscape makes it easy to describe graph elements with. labels of nodes and edges.

We indicate edge labels inside the dictionary describing its beginning and end, with the label key.

To display the labels, we use the list of CSS styles indicated for the stylesheet parameter. Each style corresponds to a dictionary with two keys:

  • selector takes the values of edge and node,

  • styles takes a dictionary with information about the styles.

To display the name, in the dictionary assigned to the style key, we place a key-value pair: 'content': 'data(label)'. In the example below, we additionally change the edge color to grey, adding the pair 'color': 'grey' to the dictionary.

app = dash.Dash()

app.layout = html.Div([
    cyto.Cytoscape(
            id='cytoscape-simple-graph',
            layout={'name': 'random'},
            style={'width': '100%', 'height': '600px'},
            elements=[
                {'data': {'id': 'first', 'label': 'Alice'}},
                {'data': {'id': 'second', 'label': 'Bob'}},
                {'data': {'id': 'third', 'label': 'Carol'}},
                {'data': {'id': 'fourth', 'label': 'Dan'}},
                {'data': {'source': 'first', 'target': 'second', 'label': 'WhatsApp'}},
                {'data': {'source': 'second', 'target': 'fourth', 'label': 'Messenger'}},
                {'data': {'source': 'third', 'target': 'first', 'label': 'phone call'}},
                {'data': {'source': 'fourth', 'target': 'third', 'label': 'F2F'}},
            ],
            stylesheet=[
                        {
                            'selector': 'edge',
                            'style': {'content': 'data(label)', 'color': 'grey'}
                        },
                                                {
                            'selector': 'node',
                            'style': {'content': 'data(label)'}
                        },
            ]
        )
    ])

Directed graph

Lets note that the definition of an edge assumes that, by default, the edge, has a start and an end. This information determines the direction of the graph in case we need to construct a directed graph.

Information about what the arrow of the edge should look like is also indicated inside the stylesheet parameter. To add arrows we need to change the style of the line to bezier and set their shape (in the example below we choose the vee shape).

app = dash.Dash()

app.layout = html.Div([
    cyto.Cytoscape(
            id='cytoscape-simple-graph',
            layout={'name': 'random'},
            style={'width': '100%', 'height': '600px'},
            elements=[
                {'data': {'id': 'first', 'label': 'Alice'}},
                {'data': {'id': 'second', 'label': 'Bob'}},
                {'data': {'id': 'third', 'label': 'Carol'}},
                {'data': {'id': 'fourth', 'label': 'Dan'}},
                {'data': {'source': 'first', 'target': 'second', 'label': 'WhatsApp'}},
                {'data': {'source': 'second', 'target': 'fourth', 'label': 'Messenger'}},
                {'data': {'source': 'third', 'target': 'first', 'label': 'phone call'}},
                {'data': {'source': 'fourth', 'target': 'third', 'label': 'F2F'}},
            ],
            stylesheet=[
                        {
                            'selector': 'edge',
                            'style': {
                                'curve-style': 'bezier',
                                'line-color':'#F4F3EE',
                                'source-arrow-color': '#BCB8B1',
                                'source-arrow-shape': 'vee',
                                }
                        },
                                                {
                            'selector': 'node',
                            'style': {
                                'content': 'data(label)',
                                'color': '#8A817C'
                            }
                        },
            ]
        )
    ])

Using callbacks

We mentioned earlier that graphs created with Dash Cytoscape can be easily used for user interaction thanks to callbacks.

The dash-cytoscape.Cytoscape class has properties that keep track of the last selected element and the one that was last hovered over. These properties are:

  • mouseoverNodeData stores information about the node that was last hovered over,

  • mouseoverEdgeData stores information about the edge that was last hovered over,

  • tapNodeData stores information about the last selected node,

  • tapEdgeData stores information about the last selected edge.

Below we show a simple example of an application where these properties were used to display information with callbacks:

Last updated