Skip to content

Commit 7613b94

Browse files
author
Xing
authored
Adding a Tree class for constructing trees in Cytoscape (#48)
* Create Tree.py * Added Tree as an import in Init * Added Tree object * Update based on PR Review * Changes based on PR reviews
1 parent 8b08621 commit 7613b94

File tree

4 files changed

+176
-22
lines changed

4 files changed

+176
-22
lines changed

dash_cytoscape/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# noinspection PyUnresolvedReferences
1010
from ._imports_ import *
1111
from ._imports_ import __all__
12+
from . import utils
1213

1314

1415
if not hasattr(_dash, 'development'):

dash_cytoscape/utils/Tree.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from collections import deque
2+
3+
4+
class Tree(object):
5+
def __init__(self, node_id, children=None, data=None, edge_data=None):
6+
"""
7+
A class to facilitate tree manipulation in Cytoscape.
8+
:param node_id: The ID of this tree, passed to the node data dict
9+
:param children: The children of this tree, also Tree objects
10+
:param data: Dictionary passed to this tree's node data dict
11+
:param edge_data: Dictionary passed to the data dict of the edge connecting this tree to its
12+
parent
13+
"""
14+
if not children:
15+
children = []
16+
if not data:
17+
data = {}
18+
if not edge_data:
19+
edge_data = {}
20+
21+
self.node_id = node_id
22+
self.children = children
23+
self.data = data
24+
self.edge_data = edge_data
25+
self.index = {}
26+
27+
def _dfs(self, search_id):
28+
if self.node_id == search_id:
29+
return self
30+
elif self.is_leaf():
31+
return None
32+
else:
33+
for child in self.children:
34+
result = child.dfs()
35+
if result:
36+
return result
37+
38+
return None
39+
40+
def _bfs(self, search_id):
41+
stack = deque([self])
42+
43+
while stack:
44+
tree = stack.popleft()
45+
46+
if tree.node_id == search_id:
47+
return tree
48+
49+
if not tree.is_leaf():
50+
for child in tree.children:
51+
stack.append(child)
52+
53+
return None
54+
55+
def is_leaf(self):
56+
"""
57+
:return: If the Tree is a leaf or not
58+
"""
59+
return not self.children
60+
61+
def add_child(self, child):
62+
"""
63+
Add a single child to the children of a Tree.
64+
:param child: Tree object
65+
"""
66+
self.children.append(child)
67+
68+
def add_children(self, children):
69+
"""
70+
Add a list of children to the current children of a Tree.
71+
:param children: List of Tree objects
72+
"""
73+
self.children.extend(children)
74+
75+
def get_edges(self):
76+
"""
77+
Get all the edges of the tree in Cytoscape JSON format.
78+
:return: List of dictionaries, each specifying an edge
79+
"""
80+
edges = [
81+
{
82+
'data': {
83+
'source': self.node_id,
84+
'target': child.node_id,
85+
**self.edge_data
86+
}
87+
}
88+
for child in self.children
89+
]
90+
91+
for child in self.children:
92+
edges.extend(child.get_edges())
93+
94+
return edges
95+
96+
def get_nodes(self):
97+
"""
98+
Get all the nodes of the tree in Cytoscape JSON format.
99+
:return: List of dictionaries, each specifying a node
100+
"""
101+
nodes = [
102+
{
103+
'data': {
104+
'id': self.node_id,
105+
**self.data
106+
}
107+
}
108+
]
109+
110+
for child in self.children:
111+
nodes.extend(child.get_nodes())
112+
113+
return nodes
114+
115+
def get_elements(self):
116+
"""
117+
Get all the elements of the tree in Cytoscape JSON format.
118+
:return: List of dictionaries, each specifying an element
119+
"""
120+
return self.get_nodes() + self.get_edges()
121+
122+
def find_by_id(self, search_id, method='bfs'):
123+
"""
124+
Find a Tree object by its ID.
125+
:param search_id: the queried ID
126+
:param method: Which traversal method to use. Either "bfs" or "dfs"
127+
:return: Tree object if found, None otherwise
128+
"""
129+
method = method.lower()
130+
131+
if method == 'bfs':
132+
return self._bfs(search_id)
133+
elif method == 'dfs':
134+
return self._dfs(search_id)
135+
else:
136+
raise ValueError('Unknown traversal method')
137+
138+
def create_index(self):
139+
"""
140+
Generate the index of a Tree, and set it in place. If there was a previous index, it is
141+
erased. This uses a BFS traversal. Please note that when a child is added to the tree,
142+
the index is not regenerated. Furthermore, an index assigned to a parent cannot be
143+
accessed by its children, and vice-versa.
144+
:return: Dictionary mapping node_id to Tree object
145+
"""
146+
stack = deque([self])
147+
self.index = {}
148+
149+
while stack:
150+
tree = stack.popleft()
151+
self.index[tree.node_id] = tree
152+
153+
if not tree.is_leaf():
154+
for child in tree.children:
155+
stack.append(child)
156+
157+
return self.index

dash_cytoscape/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .Tree import Tree

demos/usage-dag-edges.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dash_core_components as dcc
44
import dash_html_components as html
55

6+
from dash_cytoscape.utils import Tree
67
import dash_cytoscape as cyto
78

89
import demos.dash_reusable_components as drc
@@ -16,28 +17,22 @@ def flatten(z):
1617
server = app.server
1718

1819
# Define a tree in the adjacency list format
19-
adj_dict = {
20-
'root': ['l', 'r'],
21-
'l': ['ll', 'lr'],
22-
'r': ['rl', 'rr'],
23-
'll': ['lll', 'llr'],
24-
'rr': ['rrl', 'rrr']
25-
}
26-
27-
# Flatten values, then only keep the unique entries
28-
node_ids = list(set(flatten(adj_dict.values()))) + ['root']
29-
30-
nodes = [{'data': {'id': node_id}} for node_id in node_ids]
31-
edges = flatten([
32-
[
33-
{'data': {'source': src, 'target': tar}}
34-
for tar in adj_dict[src]
35-
]
36-
for src in adj_dict
20+
tree = Tree('a', [
21+
Tree('b', [
22+
Tree('c'),
23+
Tree('d')
24+
]),
25+
Tree('e', [
26+
Tree('g')
27+
]),
28+
Tree('f'),
29+
Tree('h', [
30+
Tree('i', [
31+
Tree('j')
32+
])
33+
])
3734
])
3835

39-
elements = nodes + edges
40-
4136
# Start the app
4237
app.layout = html.Div([
4338
dcc.Dropdown(
@@ -56,10 +51,10 @@ def flatten(z):
5651
),
5752
cyto.Cytoscape(
5853
id='cytoscape',
59-
elements=elements,
54+
elements=tree.get_elements(),
6055
layout={
6156
'name': 'breadthfirst',
62-
'roots': ['root']
57+
'roots': ['a']
6358
},
6459
style={
6560
'height': '95vh',

0 commit comments

Comments
 (0)