Skip to content

Commit f8b60dd

Browse files
author
Xing Han Lu
authored
Merge pull request #17 from plotly/dev
Phylogeny Update (dev -> master merge)
2 parents e43b7ba + 4f7e2a3 commit f8b60dd

File tree

2 files changed

+127
-71
lines changed

2 files changed

+127
-71
lines changed

TODO.md

Lines changed: 0 additions & 37 deletions
This file was deleted.

demos/usage-phylogeny.py

Lines changed: 127 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import math
2+
13
import dash_cytoscape
24
import dash
35
from dash.dependencies import Input, Output
@@ -7,52 +9,122 @@
79
try:
810
from Bio import Phylo
911
except 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
49123
tree = 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

57129
stylesheet = [
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
91164
app = dash.Dash(__name__)
92165

93166
app.scripts.config.serve_locally = True
94167
app.css.config.serve_locally = True
95168

96-
97169
app.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+
111204
if __name__ == '__main__':
112205
app.run_server(debug=True)

0 commit comments

Comments
 (0)