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
55import dash
6- from typing import Tuple
6+ from typing import Iterable , Tuple
77from dash import Input , Output , State , dcc , html
88
99from git_analysis .java_type import HierarchyType
1010from graph .graph_logic import *
1111from .app import app , cache , field
12- from .graph_import import graph_params_to_state
12+ from .graph_import import graph_params_to_state , GraphState
1313import dash_cytoscape as cyto
1414
1515import json
3434 "selector" : 'edge' ,
3535 'style' : {
3636 "curve-style" : "bezier" ,
37+ "target-arrow-shape" : "vee" ,
3738 "opacity" : 0.45 ,
3839 'z-index' : 5000
3940 }
124125 'width' : '100%'
125126 },
126127 'graphView' : {
127- 'flex' : '8'
128+ 'flex' : '8' ,
129+ 'border' : '1px solid black'
128130 },
129131 'tabView' : {
130132 'flex' : '4'
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' ,
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 (
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)
252261def 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