1+ import math
2+
13import dash_cytoscape
24import dash
35from dash .dependencies import Input , Output
79try :
810 from Bio import Phylo
911except ModuleNotFoundError as e :
10- print (e , "Please make sure you have biopython installed correctly before running this example." )
12+ print (e ,
13+ "Please make sure biopython is installed correctly before running this example." )
1114 exit (1 )
1215
1316
14- def generate_elements (tree ):
15- elements = []
16-
17- def _add_to_elements (clade , clade_id ):
17+ def generate_elements (tree , xlen = 30 , ylen = 30 , grabbable = False ):
18+ def get_col_positions (tree , column_width = 80 ):
19+ taxa = tree .get_terminals ()
20+
21+ # Some constants for the drawing calculations
22+ max_label_width = max (len (str (taxon )) for taxon in taxa )
23+ drawing_width = column_width - max_label_width - 1
24+
25+ """Create a mapping of each clade to its column position."""
26+ depths = tree .depths ()
27+ # If there are no branch lengths, assume unit branch lengths
28+ if not max (depths .values ()):
29+ depths = tree .depths (unit_branch_lengths = True )
30+ # Potential drawing overflow due to rounding -- 1 char per tree layer
31+ fudge_margin = int (math .ceil (math .log (len (taxa ), 2 )))
32+ cols_per_branch_unit = ((drawing_width - fudge_margin ) /
33+ float (max (depths .values ())))
34+ return dict ((clade , int (blen * cols_per_branch_unit + 1.0 ))
35+ for clade , blen in depths .items ())
36+
37+ def get_row_positions (tree ):
38+ taxa = tree .get_terminals ()
39+ positions = dict ((taxon , 2 * idx ) for idx , taxon in enumerate (taxa ))
40+
41+ def calc_row (clade ):
42+ for subclade in clade :
43+ if subclade not in positions :
44+ calc_row (subclade )
45+ positions [clade ] = ((positions [clade .clades [0 ]] +
46+ positions [clade .clades [- 1 ]]) // 2 )
47+
48+ calc_row (tree .root )
49+ return positions
50+
51+ def add_to_elements (clade , clade_id ):
1852 children = clade .clades
1953
20- cy_source = {"data" : {"id" : clade_id }, 'classes' : 'nonterminal' }
21- elements .append (cy_source )
54+ pos_x = col_positions [clade ] * xlen
55+ pos_y = row_positions [clade ] * ylen
56+
57+ cy_source = {
58+ "data" : {"id" : clade_id },
59+ 'position' : {'x' : pos_x , 'y' : pos_y },
60+ 'classes' : 'nonterminal' ,
61+ 'grabbable' : grabbable
62+ }
63+ nodes .append (cy_source )
2264
2365 if clade .is_terminal ():
2466 cy_source ['data' ]['name' ] = clade .name
2567 cy_source ['classes' ] = 'terminal'
2668
27- for n , child in enumerate (children , 1 ):
28- child_id = len (elements ) + n
29-
30- cy_edge = {'data' : {
31- 'source' : clade_id ,
32- 'target' : child_id ,
33- 'length' : clade .branch_length
34- }}
69+ for n , child in enumerate (children ):
70+ # The "support" node is on the same column as the parent clade,
71+ # and on the same row as the child clade. It is used to create the
72+ # 90 degree angle between the parent and the children.
73+ # Edge config: parent -> support -> child
74+
75+ support_id = clade_id + 's' + str (n )
76+ child_id = clade_id + 'c' + str (n )
77+ pos_y_child = row_positions [child ] * ylen
78+
79+ cy_support_node = {
80+ 'data' : {'id' : support_id },
81+ 'position' : {'x' : pos_x , 'y' : pos_y_child },
82+ 'grabbable' : grabbable ,
83+ 'classes' : 'support'
84+ }
85+
86+ cy_support_edge = {
87+ 'data' : {
88+ 'source' : clade_id ,
89+ 'target' : support_id ,
90+ 'sourceCladeId' : clade_id
91+ },
92+ }
93+
94+ cy_edge = {
95+ 'data' : {
96+ 'source' : support_id ,
97+ 'target' : child_id ,
98+ 'length' : clade .branch_length ,
99+ 'sourceCladeId' : clade_id
100+ },
101+ }
35102
36103 if clade .confidence and clade .confidence .value :
37104 cy_source ['data' ]['confidence' ] = clade .confidence .value
38105
39- elements .extend ([cy_edge ])
106+ nodes .append (cy_support_node )
107+ edges .extend ([cy_support_edge , cy_edge ])
40108
41- _add_to_elements (child , child_id )
109+ add_to_elements (child , child_id )
42110
43- _add_to_elements (tree .clade , 0 )
111+ col_positions = get_col_positions (tree )
112+ row_positions = get_row_positions (tree )
44113
45- return elements
114+ nodes = []
115+ edges = []
116+
117+ add_to_elements (tree .clade , 'r' )
118+
119+ return nodes , edges
46120
47121
48122# Define elements, stylesheet and layout
49123tree = Phylo .read ('data/apaf.xml' , 'phyloxml' )
50- elements = generate_elements (tree )
124+ nodes , edges = generate_elements (tree )
125+ elements = nodes + edges
51126
52- layout = {
53- 'name' : 'breadthfirst' ,
54- 'directed' : True
55- }
127+ layout = {'name' : 'preset' }
56128
57129stylesheet = [
58130 {
@@ -64,36 +136,36 @@ def _add_to_elements(clade, clade_id):
64136 "text-valign" : "top" ,
65137 }
66138 },
139+ {
140+ 'selector' : '.support' ,
141+ 'style' : {'background-opacity' : 0 }
142+ },
67143 {
68144 'selector' : 'edge' ,
69145 'style' : {
70- "source-endpoint" : "outside-to-node" ,
146+ "source-endpoint" : "inside-to-node" ,
147+ "target-endpoint" : "inside-to-node" ,
71148 }
72149 },
73150 {
74151 'selector' : '.terminal' ,
75152 'style' : {
76153 'label' : 'data(name)' ,
77- "shape" : "roundrectangle" ,
78- "width" : 115 ,
79- "height" : 25 ,
154+ 'width' : 10 ,
155+ 'height' : 10 ,
80156 "text-valign" : "center" ,
81- 'background-color' : 'white' ,
82- "border-width" : 1.5 ,
83- "border-style" : "solid" ,
84- "border-opacity" : 1 ,
157+ "text-halign" : "right" ,
158+ 'background-color' : '#222222'
85159 }
86160 }
87161]
88162
89-
90163# Start the app
91164app = dash .Dash (__name__ )
92165
93166app .scripts .config .serve_locally = True
94167app .css .config .serve_locally = True
95168
96-
97169app .layout = html .Div ([
98170 dash_cytoscape .Cytoscape (
99171 id = 'cytoscape' ,
@@ -108,5 +180,26 @@ def _add_to_elements(clade, clade_id):
108180])
109181
110182
183+ @app .callback (Output ('cytoscape' , 'stylesheet' ),
184+ [Input ('cytoscape' , 'mouseoverEdgeData' )])
185+ def color_children (edgeData ):
186+ if not edgeData :
187+ return stylesheet
188+
189+ if 's' in edgeData ['source' ]:
190+ val = edgeData ['source' ].split ('s' )[0 ]
191+ else :
192+ val = edgeData ['source' ]
193+
194+ children_style = [{
195+ 'selector' : f'edge[source *= "{ val } "]' ,
196+ 'style' : {
197+ 'line-color' : 'blue'
198+ }
199+ }]
200+
201+ return stylesheet + children_style
202+
203+
111204if __name__ == '__main__' :
112205 app .run_server (debug = True )
0 commit comments