Combining Interactivity + Differentiating Edge Colors in Dash Cytoscape Plot

Hi!

In theory, I know it is possible to combine user interactivity and styling with callbacks in a Cytoscape but I’m having a bit of trouble figuring this out. In brief, when users click any node, I want that node and its connections to other nodes be highlighted, but the connected edges should also be colored based on the class of the edge AND all other unselected nodes + edges should be brought to the background (i.e., muted.)

My questions are:

  • How can I retrieve the properties (mainly, classes) of nodes and edges from a Cytoscape callback?
  • How can I selectively color edges and nodes based on the node a user selects?
  • How can I integrate nodes without connections in the network without visible loops (right now, I have them as self-loops where the source equals the target)?
  • Can the colors of disabled buttons (e.g. the color of a disabled success button as seen in Button - dbc docs) be used as colors for edges?

Image of dashboard loaded at initial state:

I first established the styling spreadsheet dictionary for the Cytoscape, with the selectors .yellow, .green, and .blue corresponding to my edge classes:

default_stylesheet = [
{
“selector”: ‘node’,
‘style’: {
“opacity”: 0.65,
}
},
{
“selector”: ‘edge’,
‘style’: {
“curve-style”: “bezier”,
“opacity”: 0.65
}
},
{
‘selector’: ‘.yellow’,
‘style’: {
‘background-color’: ‘yellow’,
‘line-color’: ‘yellow’
}
},
{
‘selector’: ‘.green’,
‘style’: {
‘background-color’: ‘green’,
‘line-color’: ‘green’
}
},
{
‘selector’: ‘.blue’,
‘style’: {
‘background-color’: ‘blue’,
‘line-color’: ‘blue’
}
},
{
‘selector’: ‘.person’,
‘style’: {
‘shape’: ‘pentagon’,
}
}, {
‘selector’: ‘.store’,
‘style’: {
‘shape’: ‘ellipse’,
}
}
]

And then I created a dbc.Row with the Cytoscape plot and a few widgets to toggle the node shapes based on assigned classes:

dbc.Row([
    dbc.Col([
        html.H5('Food Network', style={'textAlign': 'center', 'margin': '3px 0px 0px 0px'}),
        cyto.Cytoscape(
            id='cytoscape',
            elements=cy_edges + cy_nodes,
            style={'height': '48vh'}, #'width': '100%'
        ),
    
        dbc.Row(children=[
            dbc.Col([
                html.P('Layout', style={'textAlign': 'center', 'margin': '0px'}),
                dcc.Dropdown(
                    id='dropdown-layout',
                    options=drc.DropdownOptionsList(
                        'random',
                        'grid',
                        'circle',
                        'concentric',
                        'breadthfirst',
                        'cose'
                    ),
                    optionHeight=25,
                    value='grid',
                    clearable=False,
                    style={'width': '120px', 'height': '10px', 'padding-left': '14px', 'padding-top': '1px'}, #TRBL
                    ),
                ], width=3, style={'display': 'inline-block', 'justifyContent': 'center', 'height': '1vh'}),

            dbc.Col([
                html.P('Person Shape', style={'textAlign': 'center', 'margin': '0px'}),
                dcc.Dropdown(
                    id='dropdown-person-shape',
                    options=drc.DropdownOptionsList(
                        'ellipse',
                        'triangle',
                        'rectangle',
                        'diamond',
                        'pentagon',
                        'hexagon',
                        'heptagon',
                        'octagon',
                        'star',
                        'polygon',
                    ),
                    optionHeight=25,
                    value='pentagon',
                    clearable=False,
                    style={'width': '120px', 'height': '10px', 'padding-left': '12px', 'padding-top': '1px'},
                    ),
                ], width=3, style={'display': 'inline-block', 'justifyContent': 'center', 'height': '1vh'}),

            dbc.Col([
                html.P('Store Shape', style={'textAlign': 'center', 'margin': '0px'}),
                dcc.Dropdown(
                    id='dropdown-store-shape',
                    options=drc.DropdownOptionsList(
                        'ellipse',
                        'triangle',
                        'rectangle',
                        'diamond',
                        'pentagon',
                        'hexagon',
                        'heptagon',
                        'octagon',
                        'star',
                        'polygon',
                    ),
                    optionHeight=25,
                    value='ellipse',
                    clearable=False,
                    style={'width': '120px', 'height': '10px', 'padding-left': '12px', 'padding-top': '1px'},
                    ),
                ], width=3, style={'display': 'inline-block', 'justifyContent': 'center', 'height': '1vh'}),
            ])
        ], width = 6, style={'border': '1px solid', 'height': '59vh', 'display': 'inline-block', 'align-items': 'center', 'justify-content': 'center'}),

This returns a plot as shown in the attached screenshot above. My trouble now is I’m having difficulty understanding which property from a callback of the selected node I can use to identify the class of the connected edges to be highlighted and simultaneously for all other nodes and edges to be muted in their color (i.e., my brain cannot figure out how to preserve coloring when a user clicks any node):

I know the issue is with my callback function (I copied chunks from dash-cytoscape/usage-stylesheet.py at master · plotly/dash-cytoscape · GitHub) and I know the last two chunks are likely my culprit but I’m a bit stumped with how to go about this now…:

@app.callback(Output(‘cytoscape’, ‘stylesheet’),
[Input(‘cytoscape’, ‘tapNode’),
Input(‘dropdown-person-shape’, ‘value’),
Input(‘dropdown-store-shape’, ‘value’)])
def generate_stylesheet(node, person_shape, store_shape):

if not node:
    return default_stylesheet
      
stylesheet = [{
    'selector': '.person',
    'style': {
        'shape': person_shape,
    }
},  {
    'selector': '.store',
    'style': {
        'shape': store_shape,
    }
},
    {
    'selector': 'edge',
    'style': {
        'opacity': 0.2,
        "curve-style": "bezier",
    }
}, {
    "selector": 'node[id = "{}"]'.format(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
    }
}]

  return stylesheet    

I’m not sure if I’m quite articulating my problem in an easily understandable manner so please let me know if I can clarify anything at all! Super thanks for any guidance, suggestions, or help you could provide me :slight_smile:

I figured out a very janky and inefficient way to get it to do what I want – after looking at the Cytoscape documentation, it seems that retrieving edge properties (e.g., classes) based on user activity only occurs when a user clicks on an edge (so, it is not possible with user activity with nodes.) This does not work for my needs. What I did instead was essentially:

  • for each edge:
  • based on the particular node class, identify the corresponding edge in a list of dictionaries of edges (cy_edges):

color_class = next((item[‘classes’] for i, item in enumerate(cy_edges) if (str(item[‘data’][‘source’]) == str(edge[‘source’])) & (str(item[‘data’][‘target’]) == str(edge[‘target’]))), None)

  • the class corresponds to a class:color dictionary, where the key is an edge class and the value is some color (hex code or verbal descriptor,) I created earlier in my script:

edge_colors = {‘physical’: ‘#5cb85c’, ‘virtual’: ‘#0275d8’, ‘hybrid’: ‘yellow’, ‘self’: ‘grey’}

  • then, I append an existing stylesheet for the cytoscape with a unique style for the particular node (and edge; I decided against doing this for my own needs.)

  • my final product:

But if anyone can help me with my last two questions, I’d greatly appreciate it!