Skip to content

Commit 922c003

Browse files
committed
adding nodes with edges seems to work
1 parent b46f615 commit 922c003

File tree

1 file changed

+101
-14
lines changed

1 file changed

+101
-14
lines changed

viz/graph.py

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
""" Based on https://raw.githubusercontent.com/plotly/dash-cytoscape/master/usage-elements.py
2-
modified to use our own graph
2+
Defines the graph visualization itself, and a side control panel.
33
"""
44

55
import dash
6-
from typing import Tuple
6+
from typing import Iterable, Tuple
77
from dash import Input, Output, State, dcc, html
88

99
from git_analysis.java_type import HierarchyType
1010
from graph.graph_logic import *
1111
from .app import app, cache, field
12-
from .graph_import import graph_params_to_state
12+
from .graph_import import graph_params_to_state, GraphState
1313
import dash_cytoscape as cyto
1414

1515
import json
@@ -34,6 +34,7 @@
3434
"selector": 'edge',
3535
'style': {
3636
"curve-style": "bezier",
37+
"target-arrow-shape": "vee",
3738
"opacity": 0.45,
3839
'z-index': 5000
3940
}
@@ -124,7 +125,8 @@
124125
'width': '100%'
125126
},
126127
'graphView': {
127-
'flex': '8'
128+
'flex': '8',
129+
'border': '1px solid black'
128130
},
129131
'tabView': {
130132
'flex': '4'
@@ -133,7 +135,7 @@
133135

134136

135137

136-
graph = html.Div(id="graphAndTabs", style={"display": "none"}, children=[
138+
graph = html.Div(id="graphAndTabs", style={"display": "none"}, children=[
137139
html.Div(style=styles["graphView"], children=[
138140
cyto.Cytoscape(
139141
id='cytoscape',
@@ -145,7 +147,7 @@
145147
)
146148
]),
147149

148-
html.Div(style=styles["tabView"], children=[
150+
html.Div(id="tabView", style=styles["tabView"], children=[
149151
dcc.Tabs(id='tabs', children=[
150152
dcc.Tab(label='Control Panel', children=[
151153
drc.NamedDropdown(
@@ -177,7 +179,14 @@
177179
dcc.Dropdown(id="typedef_dropdown", placeholder="Type def(class/interface/enum) name", options=[]),
178180
dcc.Dropdown(id="method_dropdown", placeholder="Method name", options=[]),
179181
html.Button(id="focus_button", children="Focus")
180-
]))
182+
])),
183+
html.P("Adding many nodes to the graph. The nodes are given from alternating communities, " +
184+
"and sorted by page-ranks within each community."),
185+
field("Maximal number of nodes to add",
186+
dcc.Slider(id="num_nodes_add", min=0, max=1000)),
187+
dcc.Checklist(id="add_edges", options=[
188+
"Add edges spanned by these nodes too(can be heavy)"], value=[]),
189+
html.Button(id="add_button", children="Add"),
181190
]),
182191

183192
dcc.Tab(label='JSON', children=[
@@ -250,6 +259,7 @@ def update_cytoscape_layout(layout):
250259
)
251260
)
252261
def update_search_options(graph_active, package, typedef, method, graph_params, old_options):
262+
""" Callback for updating dropdowns for searching a class/method/interface """
253263
new_options = old_options or { "package": [], "typedef": [], "method": []}
254264
if not graph_params or not graph_active:
255265
return { "new_options": new_options, "focus_disabled": True }
@@ -278,26 +288,99 @@ def dfv_to_options(dfv: pd.Series):
278288
)
279289

280290
return { "new_options": new_options, "focus_disabled": not can_focus }
281-
291+
292+
def add_nodes(state: GraphState, elements, num_nodes_to_add, add_edges_opt):
293+
""" Adds nodes(and possibly their edges) to the graph.
294+
The number of nodes being added is divided evenly among all communities,
295+
and within each community, added in descending order of page-rank values.
296+
This should ensure that the nodes added to the visualization are more
297+
representative, without biasing towards a particular dominant community.
298+
"""
299+
if not num_nodes_to_add:
300+
return elements
301+
302+
# first, determine candidate nodes -
303+
# those not already present in the graph
304+
existing_node_names = set(
305+
el.get("data").get("name") for el in elements
306+
)
307+
308+
df = state.vertices_by_communities_prs
309+
df = df[~(df["name"].isin(existing_node_names))]
310+
311+
312+
if len(df) == 0:
313+
return elements
314+
315+
# split the number of nodes added within each community,
316+
# evenly.
317+
communities = df.index.get_level_values(0).unique()
318+
n_communities = len(communities)
319+
take_per_comm = num_nodes_to_add // n_communities
320+
take_remainder = num_nodes_to_add % n_communities
321+
322+
node_names = set()
323+
for ix, comm_ix in enumerate(communities):
324+
to_take = take_per_comm
325+
if ix == 0:
326+
to_take += take_remainder
327+
node_names.update(df.loc[comm_ix].iloc[:to_take]["name"])
328+
329+
330+
new_node_names = node_names - existing_node_names
331+
new_nodes = state.graph.vs.select(name_in=new_node_names)
332+
333+
new_elements = elements + [igraph_vert_to_cyto(state.graph, node, []) for node in new_nodes ]
334+
new_edges = []
335+
if add_edges_opt:
336+
subgraph = state.graph.induced_subgraph(new_nodes)
337+
new_edges = get_filtered_edges(elements, subgraph.es)
338+
new_elements.extend(igraph_edge_to_cyto(subgraph, edge, []) for edge in new_edges)
339+
340+
return new_elements
341+
342+
def get_filtered_edges(elements, new_edges: Iterable[igraph.Edge]) -> Iterable[igraph.Edge]:
343+
"""" Given the current cytoscape elements, and a set of edges
344+
to be added, filters them to avoid duplicate edges.
345+
"""
346+
existing_edges = set(
347+
(el["data"]["source"], el["data"]["target"])
348+
for el in elements
349+
if el["data"].get("source")
350+
)
351+
for edge in new_edges:
352+
from_name = edge.source_vertex["name"]
353+
to_name = edge.target_vertex["name"]
354+
if (from_name, to_name) not in existing_edges:
355+
yield edge
282356

283357
@app.callback(Output('cytoscape', 'elements'),
284358
inputs=dict(
285359
graph_active=Input("graph_active", "data"),
286360
graph_params=Input("graph_params", "data"),
287361
nodeData=Input("cytoscape", "tapNodeData"),
288362
focus=Input("focus_button", "n_clicks"),
363+
add_edge=Input("add_button", "n_clicks")
289364
),
290365
state=dict(
291366
focus_values=[State("package_dropdown", "value"),
292367
State("typedef_dropdown", "value"),
293368
State("method_dropdown", "value")],
294369
elements=State("cytoscape", "elements"),
295-
expansion_mode=State("expansion_mode", "value")
370+
expansion_mode=State("expansion_mode", "value"),
371+
num_nodes_to_add=State("num_nodes_add", "value"),
372+
add_edges_opt=State("add_edges", "value"),
296373
))
297-
def generate_elements(graph_active, graph_params, nodeData, focus, focus_values, elements, expansion_mode):
374+
def generate_elements(graph_active, graph_params, nodeData, focus, add_edge, focus_values, elements, expansion_mode,
375+
num_nodes_to_add, add_edges_opt):
376+
""" This callback is responsible for generating the graph's elements, therefore
377+
it needs to respond to every input that can affect the graph - hence
378+
the huge amount of inputs.
379+
"""
298380
if not graph_active:
299381
return []
300-
graph = graph_params_to_state(graph_params).graph
382+
state = graph_params_to_state(graph_params)
383+
graph = state.graph
301384

302385
ctx = dash.callback_context
303386
if not ctx.triggered:
@@ -309,6 +392,7 @@ def generate_elements(graph_active, graph_params, nodeData, focus, focus_values,
309392
tappedANode = any("cytoscape.tapNodeData" in prop['prop_id'] for prop in ctx.triggered)
310393

311394
focus_node = "focus_button" in changed_inputs
395+
add_edge = "add_button" in changed_inputs
312396
reload_graph = any(v in changed_inputs for v in ("import_dir", "graph_params"))
313397
if focus_node:
314398
name = ".".join(str(val) for val in focus_values if val)
@@ -326,7 +410,8 @@ def generate_elements(graph_active, graph_params, nodeData, focus, focus_values,
326410
elements.append(igraph_vert_to_cyto(graph, graph.vs.find(name=name), classes=["genesis"]))
327411

328412
return elements
329-
413+
elif add_edge:
414+
return add_nodes(state, elements, num_nodes_to_add, add_edges_opt)
330415
elif (not nodeData) or reload_graph:
331416
print("Graph is being reloaded")
332417
return [igraph_vert_to_cyto(graph, graph.vs[0], classes=["genesis"])]
@@ -346,7 +431,8 @@ def generate_elements(graph_active, graph_params, nodeData, focus, focus_values,
346431

347432
neigh_nodes = graph.neighbors(nodeData['id'], expansion_mode)
348433
neigh_names = set(graph.vs[ix]["name"] for ix in neigh_nodes)
349-
neigh_edges = graph.incident(nodeData['id'], expansion_mode)
434+
neigh_edges = get_filtered_edges(elements, (graph.es[ix]
435+
for ix in graph.incident(nodeData['id'], expansion_mode)))
350436
node_class, edge_class = "", ""
351437
if expansion_mode == "in":
352438
node_class, edge_class = "followerNode", "followerEdge"
@@ -355,7 +441,8 @@ def generate_elements(graph_active, graph_params, nodeData, focus, focus_values,
355441

356442
if do_expand:
357443
elements.extend(igraph_vert_to_cyto(graph, graph.vs[node_ix], classes=[node_class, "selneighbor"]) for node_ix in neigh_nodes)
358-
elements.extend(igraph_edge_to_cyto(graph, graph.es[edge_ix], classes=[edge_class, "selneighbor"]) for edge_ix in neigh_edges)
444+
elements.extend(igraph_edge_to_cyto(graph, edge, classes=[edge_class, "selneighbor"])
445+
for edge in neigh_edges)
359446

360447
for element in elements:
361448
el_id = element.get('data').get('id')

0 commit comments

Comments
 (0)