"""
Network visualization utilities using Dash Cytoscape.
Creates Cytoscape elements and container for displaying bait–prey
interaction networks with selectable layouts and styles.
"""
from dash import dcc, html, callback
import dash_bootstrap_components as dbc
import dash_cytoscape as cyto
# Load extra layouts
cyto.load_extra_layouts()
[docs]
def get_cytoscape_elements_and_ints(interaction_data):
"""Convert interaction table to Cytoscape elements and a dict index.
:param interaction_data: DataFrame with columns including ``Bait``, ``Prey``, ``PreyGene``, ``AvgSpec``.
:returns: Tuple ``(elements, interactions)`` where elements is a list of nodes/edges and interactions is a nested dict.
"""
cy_edges = []
cy_nodes = []
keepi = []
for b in interaction_data['Bait'].unique():
keepi.extend(list(interaction_data[interaction_data['Bait']==b].head(100).index))
interaction_data = interaction_data[interaction_data.index.isin(keepi)]
interactions = {}
added: set = set()
for _,row in interaction_data.iterrows():
bname = row['Bait']
pname = row['Prey']
if bname not in interactions:
interactions[bname] = {}
interactions[bname][pname] = (row['PreyGene'], row['AvgSpec'])
cy_edge = {'data': {'source': bname, 'target': pname, 'int': f'{bname}-{pname}'}}
pnode = {'data': {'id': pname, 'label': row['PreyGene']}, 'classes': 'preyNode'}
bnode = {'data': {'id': bname, 'label': bname}, 'classes': 'baitNode'}
if pname not in added:
added.add(pname)
cy_nodes.append(pnode)
if bname not in added:
added.add(bname)
cy_nodes.append(bnode)
cy_edges.append(cy_edge)
created_elements = cy_nodes#[genesis_node]
created_elements.extend(cy_edges)
return (created_elements, interactions)
[docs]
def get_stylesheet():
"""Return the default Cytoscape stylesheet for bait/prey nodes.
:returns: List of stylesheet dictionaries.
"""
return [
{"selector": "node", "style": {"opacity": 0.65, "z-index": 9999}},
{
"selector": "edge",
"style": {"curve-style": "bezier", "opacity": 0.45, "z-index": 5000},
},
{"selector": ".preyNode", "style": {"font-size": 12, "background-color": "#0074D9", "label": "data(label)"}},
{
"selector": ".preyEdge",
"style": {
"line-color": "#0074D9",
},
},
{"selector": ".baitNode", "style": {"font-size": 12, "background-color": "#FF4136", "label": "data(label)"}},
{
"selector": ".baitEdge",
"style": {
"line-color": "#FF4136",
},
},
{
"selector": ":selected",
"style": {
"border-width": 2,
"border-color": "black",
"border-opacity": 1,
"opacity": 1,
"color": "black",
"font-size": 12,
"z-index": 9999,
},
},
]
[docs]
def get_cytoscape_container(cyto_elements, full_height):
"""Build a Cytoscape container with controls and the graph component.
:param cyto_elements: List of Cytoscape nodes and edges.
:param full_height: CSS height string (e.g., ``"600px"``).
:returns: Dash ``html.Div`` containing the Cytoscape graph and layout selector.
"""
default_stylesheet = get_stylesheet()
cyto_layouts = [
"grid",
"circle",
"concentric",
"breadthfirst",
"cose",
#"cose-bilkent", # Does not work for now, next version should fix.
"dagre",
"cola",
"klay",
"spread",
"euler"
]
return html.Div([
html.Div([
html.Div([
cyto.Cytoscape(
id="cytoscape",
elements=cyto_elements,
stylesheet=default_stylesheet,
style={'height': full_height, "width": "100%"},
)
],style={'width': '69%', 'display': 'inline-block'}),
html.Div([
html.P(children=f"Layout:", style={"margin-left": "3px"}),
dcc.Dropdown(
id='dropdown-layout',
options = [
{'label': val.capitalize(), 'value': val} for val in cyto_layouts
],
clearable=False,
value = 'cose'
),
html.Div(id='nodedata-div', style = {'padding-top': '15px'})
], style={'vertical-align': 'top', 'width': '30%','display': 'inline-block'})
], style={
'height': full_height,
#'overflow': 'auto',
'margin': '5px'
})
])