bigtree

🌳 Documentation¢

python-version python-compatibility Documentation Status code-coverage github-workflows license black

Big Tree Python PackageΒΆ

Tree Implementation and Methods for Python, integrated with Python list, dictionary, and pandas DataFrame.

It is pythonic, making it easy to learn and extendable to many types of workflows.


Related Links:


ComponentsΒΆ

There are 3 segments to Big Tree consisting of Tree, Binary Tree, and Directed Acyclic Graph (DAG) implementation.

For Tree implementation, there are 9 main components.

  1. 🌺 Node

    1. BaseNode, extendable class

    2. Node, BaseNode with node name attribute

  2. ✨ Constructing Tree

    1. From Node, using parent and children constructors

    2. From str, using tree in string display format

    3. From list, using paths or parent-child tuples

    4. From nested dictionary, using path or recursive structure

    5. From pandas DataFrame, using paths or parent-child columns

    6. Add nodes to existing tree using path string

    7. Add nodes and attributes to existing tree using dictionary or pandas DataFrame, using path

    8. Add only attributes to existing tree using dictionary or pandas DataFrame, using node name

  3. ➰ Traversing Tree

    1. Pre-Order Traversal

    2. Post-Order Traversal

    3. Level-Order Traversal

    4. Level-Order-Group Traversal

    5. ZigZag Traversal

    6. ZigZag-Group Traversal

  4. πŸ“ Modifying Tree

    1. Shift nodes from location to destination

    2. Copy nodes from location to destination

    3. Copy nodes from one tree to another

    4. Shift and replace nodes from location to destination

    5. Copy and replace nodes from one tree to another

  5. πŸ” Tree Search

    1. Find multiple nodes based on name, partial path, relative path, attribute value, user-defined condition

    2. Find single nodes based on name, partial path, relative path, full path, attribute value, user-defined condition

    3. Find multiple child nodes based on user-defined condition

    4. Find single child node based on name, user-defined condition

  6. πŸ”§ Helper Function

    1. Cloning tree to another Node type

    2. Prune tree

    3. Get difference between two trees

  7. πŸ“Š Plotting Tree

    1. Enhanced Reingold Tilford Algorithm to retrieve (x, y) coordinates for a tree structure

  8. πŸ”¨ Exporting Tree

    1. Print to console

    2. Export to dictionary, nested dictionary, or pandas DataFrame

    3. Export tree to dot (can save to .dot, .png, .svg, .jpeg files)

    4. Export tree to Pillow (can save to .png, .jpg)

    5. Export tree to Mermaid Flowchart (can display on .md)

  9. βœ”οΈ Workflows

    1. Sample workflows for tree demonstration!


For Binary Tree implementation, there are 3 main components. Binary Node inherits from Node, so the components in Tree implementation are also available in Binary Tree.

  1. 🌿 Node

    1. BinaryNode, Node with binary tree rules

  2. ✨ Constructing Binary Tree

    1. From list, using flattened list structure

  3. ➰ Traversing Binary Tree

    1. In-Order Traversal


For Directed Acyclic Graph (DAG) implementation, there are 4 main components.

  1. 🌼 Node

    1. DAGNode, extendable class for constructing Directed Acyclic Graph (DAG)

  2. ✨ Constructing DAG

    1. From list, containing parent-child tuples

    2. From nested dictionary

    3. From pandas DataFrame

  3. ➰ Traversing DAG

    1. Generic traversal method

  4. πŸ”¨ Exporting DAG

    1. Export to list, dictionary, or pandas DataFrame

    2. Export DAG to dot (can save to .dot, .png, .svg, .jpeg files)


InstallationΒΆ

There are two ways to install bigtree, with pip (from PyPI) or conda (from conda-forge).

b) Installation with condaΒΆ

To install bigtree with conda, run the following line in command prompt:

$ conda install -c conda-forge bigtree

Tree DemonstrationΒΆ

Here are some codes to get started.

Construct TreeΒΆ

Nodes can have attributes if they are initialized from Node, dictionary, or pandas DataFrame.

  1. From Node

Nodes can be linked to each other in the following ways:

  • Using parent and children setter methods

  • Directly passing parent or children argument

  • Using bitshift operator with the convention parent >> child or child << parent

  • Using .append(child) or .extend([child1, child2]) methods

from bigtree import Node, tree_to_dot

root = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")

root.children = [b, c]
d.parent = b

root.show()
# a
# β”œβ”€β”€ b
# β”‚   └── d
# └── c

graph = tree_to_dot(root, node_colour="gold")
graph.write_png("assets/docs/demo_tree.png")

Sample Tree Output

from bigtree import Node

root = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")

root >> b
root >> c
d << b

root.show()
# a
# β”œβ”€β”€ b
# β”‚   └── d
# └── c

Alternatively, we can directly pass parent or children argument.

from bigtree import Node

b = Node("b")
c = Node("c")
d = Node("d", parent=b)
root = Node("a", children=[b, c])

root.show(style="ascii")
# a
# |-- b
# |   +-- d
# +-- c
  1. From str

Construct nodes only.

from bigtree import str_to_tree

tree_str = """
a
β”œβ”€β”€ b
β”‚   β”œβ”€β”€ d
β”‚   └── e
β”‚       β”œβ”€β”€ g
β”‚       └── h
└── c
    └── f
"""
root = str_to_tree(tree_str)
root.show()
# a
# β”œβ”€β”€ b
# β”‚   β”œβ”€β”€ d
# β”‚   └── e
# β”‚       β”œβ”€β”€ g
# β”‚       └── h
# └── c
#     └── f
  1. From list

Construct nodes only, list can contain either full paths or tuples of parent-child names.

from bigtree import list_to_tree, list_to_tree_by_relation

root = list_to_tree(["a/b/d", "a/c"])
root.show()
# a
# β”œβ”€β”€ b
# β”‚   └── d
# └── c

root = list_to_tree_by_relation([("a", "b"), ("a", "c"), ("b", "d")])
root.show()
# a
# β”œβ”€β”€ b
# β”‚   └── d
# └── c
  1. From nested dictionary

Construct nodes using path where key is path and value is dict of node attribute names and attribute values. Dictionary can also be a recursive structure where key is node attribute names and value is node attribute values, and list of children (recursive).

from bigtree import dict_to_tree, nested_dict_to_tree

path_dict = {
   "a": {"age": 90},
   "a/b": {"age": 65},
   "a/c": {"age": 60},
   "a/b/d": {"age": 40},
}

root = dict_to_tree(path_dict)
root.show(attr_list=["age"])
# a [age=90]
# β”œβ”€β”€ b [age=65]
# β”‚   └── d [age=40]
# └── c [age=60]

path_dict = {
   "name": "a",
   "age": 90,
   "children": [
      {
         "name": "b",
         "age": 65,
         "children": [
            {"name": "d", "age": 40},
         ],
      },
      {"name": "c", "age": 60},
   ],
}

root = nested_dict_to_tree(path_dict)
root.show(attr_list=["age"])
# a [age=90]
# β”œβ”€β”€ b [age=65]
# β”‚   └── d [age=40]
# └── c [age=60]
  1. From pandas DataFrame

Construct nodes with attributes, pandas DataFrame can contain either path column or parent-child columns, and attribute columns.

import pandas as pd

from bigtree import dataframe_to_tree, dataframe_to_tree_by_relation

data = pd.DataFrame(
   [
      ["a", 90],
      ["a/b", 65],
      ["a/c", 60],
      ["a/b/d", 40],
   ],
   columns=["path", "age"],
)

root = dataframe_to_tree(data)
root.show(attr_list=["age"])
# a [age=90]
# β”œβ”€β”€ b [age=65]
# β”‚   └── d [age=40]
# └── c [age=60]

data = pd.DataFrame(
   [
      ["a", None, 90],
      ["b", "a", 65],
      ["c", "a", 60],
      ["d", "b", 40],
   ],
   columns=["child", "parent", "age"],
)

root = dataframe_to_tree_by_relation(data)
root.show(attr_list=["age"])
# a [age=90]
# β”œβ”€β”€ b [age=65]
# β”‚   └── d [age=40]
# └── c [age=60]

If tree is already created, attributes can still be added using a dictionary or pandas DataFrame!

Traverse TreeΒΆ

Tree can be traversed using pre-order, post-order, level-order, level-order-group, zigzag, zigzag-group traversal methods.

from bigtree import (
    Node,
    levelorder_iter,
    levelordergroup_iter,
    postorder_iter,
    preorder_iter,
    zigzag_iter,
    zigzaggroup_iter,
)

root = Node("a")
b = Node("b", parent=root)
c = Node("c", parent=root)
d = Node("d", parent=b)
e = Node("e", parent=b)
root.show()
# a
# β”œβ”€β”€ b
# β”‚   β”œβ”€β”€ d
# β”‚   └── e
# └── c

[node.name for node in preorder_iter(root)]
# ['a', 'b', 'd', 'e', 'c']

[node.name for node in postorder_iter(root)]
# ['d', 'e', 'b', 'c', 'a']

[node.name for node in levelorder_iter(root)]
# ['a', 'b', 'c', 'd', 'e']

[[node.name for node in node_group] for node_group in levelordergroup_iter(root)]
# [['a'], ['b', 'c'], ['d', 'e']]

[node.name for node in zigzag_iter(root)]
# ['a', 'c', 'b', 'd', 'e']

[[node.name for node in node_group] for node_group in zigzaggroup_iter(root)]
# [['a'], ['c', 'b'], ['d', 'e']]

Modify TreeΒΆ

Nodes can be shifted (with or without replacement) or copied from one path to another.

from bigtree import Node, shift_nodes, shift_and_replace_nodes

root = Node("a")
b = Node("b", parent=root)
c = Node("c", parent=root)
d = Node("d", parent=root)
root.show()
# a
# β”œβ”€β”€ b
# β”œβ”€β”€ c
# └── d

shift_nodes(
   tree=root,
   from_paths=["a/c", "a/d"],
   to_paths=["a/b/c", "a/dummy/d"],
)
root.show()
# a
# β”œβ”€β”€ b
# β”‚   └── c
# └── dummy
#     └── d

shift_and_replace_nodes(
   tree=root,
   from_paths=["a/dummy"],
   to_paths=["a/b/c"],
)
root.show()
# a
# └── b
#     └── dummy
#         └── d
from bigtree import Node, copy_nodes

root = Node("a")
b = Node("b", parent=root)
c = Node("c", parent=root)
d = Node("d", parent=root)
root.show()
# a
# β”œβ”€β”€ b
# β”œβ”€β”€ c
# └── d

copy_nodes(
   tree=root,
   from_paths=["a/c", "a/d"],
   to_paths=["a/b/c", "a/dummy/d"],
)
root.show()
# a
# β”œβ”€β”€ b
# β”‚   └── c
# β”œβ”€β”€ c
# β”œβ”€β”€ d
# └── dummy
#     └── d

Nodes can also be copied (with or without replacement) between two different trees.

from bigtree import Node, copy_nodes_from_tree_to_tree, copy_and_replace_nodes_from_tree_to_tree
root = Node("a")
b = Node("b", parent=root)
c = Node("c", parent=root)
d = Node("d", parent=root)
root.show()
# a
# β”œβ”€β”€ b
# β”œβ”€β”€ c
# └── d

root_other = Node("aa")
copy_nodes_from_tree_to_tree(
   from_tree=root,
   to_tree=root_other,
   from_paths=["a/b", "a/c", "a/d"],
   to_paths=["aa/b", "aa/b/c", "aa/dummy/d"],
)
root_other.show()
# aa
# β”œβ”€β”€ b
# β”‚   └── c
# └── dummy
#     └── d

root_other = Node("aa")
b = Node("b", parent=root_other)
c = Node("c", parent=b)
d = Node("d", parent=root_other)
root_other.show()
# aa
# β”œβ”€β”€ b
# β”‚   └── c
# └── d

copy_and_replace_nodes_from_tree_to_tree(
   from_tree=root,
   to_tree=root_other,
   from_paths=["a/b", "a/c"],
   to_paths=["aa/b/c", "aa/d"],
)
root_other.show()
# aa
# β”œβ”€β”€ b
# β”‚   └── b
# └── c

Helper UtilityΒΆ

There following are helper functions for cloning tree to another Node type, pruning tree, and getting difference between two trees.

from bigtree import BaseNode, Node, clone_tree, prune_tree, get_tree_diff

# Cloning tree from `BaseNode` to `Node` type
root = BaseNode(name="a")
b = BaseNode(name="b", parent=root)
clone_tree(root, Node)
# Node(/a, )

# Prune tree to only path a/b
root = Node("a")
b = Node("b", parent=root)
c = Node("c", parent=root)
root.show()
# a
# β”œβ”€β”€ b
# └── c

root_pruned = prune_tree(root, "a/b")
root_pruned.show()
# a
# └── b

# Get difference between two trees
root = Node("a")
b = Node("b", parent=root)
c = Node("c", parent=root)
root.show()
# a
# β”œβ”€β”€ b
# └── c

root_other = Node("a")
b_other = Node("b", parent=root_other)
root_other.show()
# a
# └── b

tree_diff = get_tree_diff(root, root_other)
tree_diff.show()
# a
# └── c (-)

tree_diff = get_tree_diff(root, root_other, only_diff=False)
tree_diff.show()
# a
# β”œβ”€β”€ b
# └── c (-)

Export TreeΒΆ

Tree can be exported to another data type.

  1. Export to nested dictionary

  2. Export to nested recursive dictionary

  3. Export to pandas DataFrame

  4. Export to dot (and png)

  5. Export to Pillow (and png)

  6. Export to Mermaid Flowchart (and md)

from bigtree import (
    Node,
    tree_to_dataframe,
    tree_to_dict,
    tree_to_dot,
    tree_to_mermaid,
    tree_to_nested_dict,
    tree_to_pillow,
)

root = Node("a", age=90)
b = Node("b", age=65, parent=root)
c = Node("c", age=60, parent=root)
d = Node("d", age=40, parent=b)
e = Node("e", age=35, parent=b)
root.show()
# a
# β”œβ”€β”€ b
# β”‚   β”œβ”€β”€ d
# β”‚   └── e
# └── c

tree_to_dict(
   root,
   name_key="name",
   parent_key="parent",
   attr_dict={"age": "person age"}
)
# {
#    '/a': {'name': 'a', 'parent': None, 'person age': 90},
#    '/a/b': {'name': 'b', 'parent': 'a', 'person age': 65},
#    '/a/b/d': {'name': 'd', 'parent': 'b', 'person age': 40},
#    '/a/b/e': {'name': 'e', 'parent': 'b', 'person age': 35},
#    '/a/c': {'name': 'c', 'parent': 'a', 'person age': 60}
# }

tree_to_nested_dict(root, all_attrs=True)
# {
#    'name': 'a',
#    'age': 90,
#    'children': [
#       {
#          'name': 'b',
#          'age': 65,
#          'children': [
#             {
#                'name': 'd',
#                'age': 40
#             },
#             {
#                'name': 'e',
#                'age': 35
#             }
#          ]
#       },
#       {
#          'name': 'c',
#          'age': 60
#       }
#    ]
# }

tree_to_dataframe(
   root,
   name_col="name",
   parent_col="parent",
   path_col="path",
   attr_dict={"age": "person age"}
)
#      path name parent  person age
# 0      /a    a   None          90
# 1    /a/b    b      a          65
# 2  /a/b/d    d      b          40
# 3  /a/b/e    e      b          35
# 4    /a/c    c      a          60

graph = tree_to_dot(root, node_colour="gold")
graph.write_png("assets/docs/demo_dot.png")

pillow_image = tree_to_pillow(root)
pillow_image.save("assets/docs/demo_pillow.png")

mermaid_md = tree_to_mermaid(root)
print(mermaid_md)
  • demo_dot.png

Sample Dot Image Output

  • demo_pillow.png

Sample Pillow Image Output

  • Mermaid flowchart

%%{ init: { 'flowchart': { 'curve': 'basis' } } }%%
flowchart TB
0("a") --> 0-0("b")
0-0 --> 0-0-0("d")
0-0 --> 0-0-1("e")
0("a") --> 0-1("c")
classDef default stroke-width:1

Binary Tree DemonstrationΒΆ

Compared to nodes in tree, nodes in Binary Tree are only allowed maximum of 2 children. Since BinaryNode extends from Node, construct, traverse, search, export methods from Node are applicable to Binary Tree as well.

Construct Binary TreeΒΆ

  1. From BinaryNode

BinaryNode can be linked to each other with parent, children, left, and right setter methods, or using bitshift operator with the convention parent_node >> child_node or child_node << parent_node.

from bigtree import BinaryNode, tree_to_dot

e = BinaryNode(5)
d = BinaryNode(4)
c = BinaryNode(3)
b = BinaryNode(2, left=d, right=e)
a = BinaryNode(1, children=[b, c])
f = BinaryNode(6, parent=c)
g = BinaryNode(7, parent=c)
h = BinaryNode(8, parent=d)

graph = tree_to_dot(a, node_colour="gold")
graph.write_png("assets/docs/demo_binarytree.png")

Sample DAG Output

  1. From list

Construct nodes only, list has similar format as heapq list.

from bigtree import list_to_binarytree

nums_list = [1, 2, 3, 4, 5, 6, 7, 8]
root = list_to_binarytree(nums_list)
root.show()
# 1
# β”œβ”€β”€ 2
# β”‚   β”œβ”€β”€ 4
# β”‚   β”‚   └── 8
# β”‚   └── 5
# └── 3
#     β”œβ”€β”€ 6
#     └── 7

Traverse Binary TreeΒΆ

In addition to the traversal methods in the usual tree, binary tree includes in-order traversal method.

from bigtree import (
    inorder_iter,
    levelorder_iter,
    levelordergroup_iter,
    list_to_binarytree,
    postorder_iter,
    preorder_iter,
    zigzag_iter,
    zigzaggroup_iter,
)

nums_list = [1, 2, 3, 4, 5, 6, 7, 8]
root = list_to_binarytree(nums_list)
root.show()
# 1
# β”œβ”€β”€ 2
# β”‚   β”œβ”€β”€ 4
# β”‚   β”‚   └── 8
# β”‚   └── 5
# └── 3
#     β”œβ”€β”€ 6
#     └── 7

[node.name for node in inorder_iter(root)]
# ['8', '4', '2', '5', '1', '6', '3', '7']

[node.name for node in preorder_iter(root)]
# ['1', '2', '4', '8', '5', '3', '6', '7']

[node.name for node in postorder_iter(root)]
# ['8', '4', '5', '2', '6', '7', '3', '1']

[node.name for node in levelorder_iter(root)]
# ['1', '2', '3', '4', '5', '6', '7', '8']

[[node.name for node in node_group] for node_group in levelordergroup_iter(root)]
# [['1'], ['2', '3'], ['4', '5', '6', '7'], ['8']]

[node.name for node in zigzag_iter(root)]
# ['1', '3', '2', '4', '5', '6', '7', '8']

[[node.name for node in node_group] for node_group in zigzaggroup_iter(root)]
# [['1'], ['3', '2'], ['4', '5', '6', '7'], ['8']]

DAG DemonstrationΒΆ

Compared to nodes in tree, nodes in DAG are able to have multiple parents.

Construct DAGΒΆ

  1. From DAGNode

DAGNode can be linked to each other with parents and children setter methods, or using bitshift operator with the convention parent_node >> child_node or child_node << parent_node.

from bigtree import DAGNode, dag_to_dot

a = DAGNode("a")
b = DAGNode("b")
c = DAGNode("c", parents=[a, b])
d = DAGNode("d", parents=[a, c])
e = DAGNode("e", parents=[d])
f = DAGNode("f", parents=[c, d])
h = DAGNode("h")
g = DAGNode("g", parents=[c], children=[h])

graph = dag_to_dot(a, node_colour="gold")
graph.write_png("assets/docs/demo_dag.png")

Sample DAG Output

  1. From list

Construct nodes only, list contains parent-child tuples.

from bigtree import list_to_dag, dag_iterator

relations_list = [
   ("a", "c"),
   ("a", "d"),
   ("b", "c"),
   ("c", "d"),
   ("d", "e")
]
dag = list_to_dag(relations_list)
print([(parent.node_name, child.node_name) for parent, child in dag_iterator(dag)])
# [('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]
  1. From nested dictionary

Construct nodes with attributes, key: child name, value: dict of parent name, child node attributes.

from bigtree import dict_to_dag, dag_iterator

relation_dict = {
   "a": {"step": 1},
   "b": {"step": 1},
   "c": {"parents": ["a", "b"], "step": 2},
   "d": {"parents": ["a", "c"], "step": 2},
   "e": {"parents": ["d"], "step": 3},
}
dag = dict_to_dag(relation_dict, parent_key="parents")
print([(parent.node_name, child.node_name) for parent, child in dag_iterator(dag)])
# [('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]
  1. From pandas DataFrame

Construct nodes with attributes, pandas DataFrame contains child column, parent column, and attribute columns.

import pandas as pd
from bigtree import dataframe_to_dag, dag_iterator

path_data = pd.DataFrame([
   ["a", None, 1],
   ["b", None, 1],
   ["c", "a", 2],
   ["c", "b", 2],
   ["d", "a", 2],
   ["d", "c", 2],
   ["e", "d", 3],
],
   columns=["child", "parent", "step"]
)
dag = dataframe_to_dag(path_data)
print([(parent.node_name, child.node_name) for parent, child in dag_iterator(dag)])
# [('a', 'd'), ('c', 'd'), ('d', 'e'), ('a', 'c'), ('b', 'c')]

Demo UsageΒΆ

There are existing implementations of workflows to showcase how bigtree can be used!

To Do ApplicationΒΆ

There are functions to:

  • Add or remove list to To-Do application

  • Add or remove item to list, default list is the β€˜General’ list

  • Prioritize a list/item by reordering them as first list/item

  • Save and import To-Do application to and from an external JSON file

  • Show To-Do application, which prints tree to console

from bigtree import AppToDo
app = AppToDo("To Do App")
app.add_item(item_name="Homework 1", list_name="School")
app.add_item(item_name=["Milk", "Bread"], list_name="Groceries", description="Urgent")
app.add_item(item_name="Cook")
app.show()
# To Do App
# β”œβ”€β”€ School
# β”‚   └── Homework 1
# β”œβ”€β”€ Groceries
# β”‚   β”œβ”€β”€ Milk [description=Urgent]
# β”‚   └── Bread [description=Urgent]
# └── General
#   └── Cook

app.save("list.json")
app2 = AppToDo.load("list.json")

Calendar ApplicationΒΆ

There are functions to:

  • Add or remove event from Calendar

  • Find event by name, or name and date

  • Display calendar, which prints events to console

  • Export calendar to pandas DataFrame

import datetime as dt
from bigtree import Calendar
calendar = Calendar("My Calendar")
calendar.add_event("Gym", "2023-01-01 18:00")
calendar.add_event("Dinner", "2023-01-01", date_format="%Y-%m-%d", budget=20)
calendar.add_event("Gym", "2023-01-02 18:00")
calendar.show()
# My Calendar
# 2023-01-01 00:00:00 - Dinner (budget: 20)
# 2023-01-01 18:00:00 - Gym
# 2023-01-02 18:00:00 - Gym

calendar.find_event("Gym")
# 2023-01-01 18:00:00 - Gym
# 2023-01-02 18:00:00 - Gym

calendar.delete_event("Gym", dt.date(2023, 1, 1))
calendar.show()
# My Calendar
# 2023-01-01 00:00:00 - Dinner (budget: 20)
# 2023-01-02 18:00:00 - Gym

data_calendar = calendar.to_dataframe()
data_calendar
#                              path    name        date      time  budget
# 0  /My Calendar/2023/01/01/Dinner  Dinner  2023-01-01  00:00:00    20.0
# 1     /My Calendar/2023/01/02/Gym     Gym  2023-01-02  18:00:00     NaN