Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.30.0] - 2025-09-05
### Added:
- Tree Construct: `nested_dict_key_to_tree` to construct trees with nested dictionary of another format.
- Tree Export: `tree_to_nested_dict_key` to export trees to nested dictionary of another format.
- Tree Render: `render_tree` to accept theme argument.
- Docs: Add pyvis html render to Tree Demonstration.

Expand Down Expand Up @@ -799,7 +803,8 @@ ignore null attribute columns.
- Utility Iterator: Tree traversal methods.
- Workflow To Do App: Tree use case with to-do list implementation.

[Unreleased]: https://github.com/kayjan/bigtree/compare/0.29.2...HEAD
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.30.0...HEAD
[0.30.0]: https://github.com/kayjan/bigtree/compare/0.29.2...0.30.0
[0.29.2]: https://github.com/kayjan/bigtree/compare/0.29.1...0.29.2
[0.29.1]: https://github.com/kayjan/bigtree/compare/0.29.0...0.29.1
[0.29.0]: https://github.com/kayjan/bigtree/compare/0.28.0...0.29.0
Expand Down
Binary file modified assets/docs/tree_construct.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion bigtree/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.29.2"
__version__ = "0.30.0"

from bigtree.binarytree.construct import list_to_binarytree
from bigtree.dag.construct import dataframe_to_dag, dict_to_dag, list_to_dag
Expand All @@ -21,6 +21,7 @@
dict_to_tree,
list_to_tree,
list_to_tree_by_relation,
nested_dict_key_to_tree,
nested_dict_to_tree,
newick_to_tree,
polars_to_tree,
Expand All @@ -37,6 +38,7 @@
tree_to_dot,
tree_to_mermaid,
tree_to_nested_dict,
tree_to_nested_dict_key,
tree_to_newick,
tree_to_pillow,
tree_to_pillow_graph,
Expand Down
2 changes: 2 additions & 0 deletions bigtree/tree/construct/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
add_dict_to_tree_by_name,
add_dict_to_tree_by_path,
dict_to_tree,
nested_dict_key_to_tree,
nested_dict_to_tree,
)
from .lists import list_to_tree, list_to_tree_by_relation
Expand All @@ -31,6 +32,7 @@
"add_dict_to_tree_by_path",
"dict_to_tree",
"nested_dict_to_tree",
"nested_dict_key_to_tree",
"list_to_tree",
"list_to_tree_by_relation",
"render_tree",
Expand Down
90 changes: 88 additions & 2 deletions bigtree/tree/construct/dictionaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"add_dict_to_tree_by_name",
"dict_to_tree",
"nested_dict_to_tree",
"nested_dict_key_to_tree",
]

T = TypeVar("T", bound=node.Node)
Expand Down Expand Up @@ -238,7 +239,7 @@ def nested_dict_to_tree(

Examples:
>>> from bigtree import nested_dict_to_tree
>>> path_dict = {
>>> nested_dict = {
... "name": "a",
... "age": 90,
... "children": [
Expand All @@ -252,7 +253,7 @@ def nested_dict_to_tree(
... ]},
... ],
... }
>>> root = nested_dict_to_tree(path_dict)
>>> root = nested_dict_to_tree(nested_dict)
>>> root.show(attr_list=["age"])
a [age=90]
└── b [age=65]
Expand Down Expand Up @@ -300,3 +301,88 @@ def _recursive_add_child(

root_node = _recursive_add_child(node_attrs)
return root_node


def nested_dict_key_to_tree(
node_attrs: Mapping[str, Mapping[str, Any]],
child_key: str = "children",
node_type: Type[T] = node.Node, # type: ignore[assignment]
) -> T:
"""Construct tree from nested recursive dictionary, where the keys are node names.

- ``key``: node name
- ``value``: dict of node attributes and node children (recursive)

Value dictionary

- ``key`` that is not ``child_key`` has node attribute as value
- ``key`` that is ``child_key`` has dictionary of node children as value (recursive)

Examples:
>>> from bigtree import nested_dict_key_to_tree
>>> nested_dict = {
... "a": {
... "age": 90,
... "children": {
... "b": {
... "age": 65,
... "children": {
... "d": {"age": 40},
... "e": {
... "age": 35,
... "children": {"g": {"age": 10}},
... },
... },
... },
... },
... }
... }
>>> root = nested_dict_key_to_tree(nested_dict)
>>> root.show(attr_list=["age"])
a [age=90]
└── b [age=65]
├── d [age=40]
└── e [age=35]
└── g [age=10]

Args:
node_attrs: node, children, and node attribute information,
key: node name
value: dictionary of node attributes and node children
child_key: key of child dict, value is type dict
node_type: node type of tree to be created

Returns:
Node
"""
assertions.assert_length(node_attrs, 1, "Dictionary", "node_attrs")

def _recursive_add_child(
child_name: str, child_dict: Mapping[str, Any], parent_node: Optional[T] = None
) -> T:
"""Recursively add child to tree, given child attributes and parent node.

Args:
child_name: child name to be added to tree
child_dict: child to be added to tree, from dictionary
parent_node: parent node to be assigned to child node

Returns:
Node
"""
child_dict = dict(child_dict)
node_children = child_dict.pop(child_key, {})
if not isinstance(node_children, Mapping):
raise TypeError(
f"child_key {child_key} should be Dict type, received {node_children}"
)
root = node_type(child_name, parent=parent_node, **child_dict)
for _child_name in node_children:
_recursive_add_child(
_child_name, node_children[_child_name], parent_node=root
)
return root

root_node_name = list(node_attrs.keys())[0]
root_node = _recursive_add_child(root_node_name, node_attrs[root_node_name])
return root_node
7 changes: 6 additions & 1 deletion bigtree/tree/export/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from .dataframes import tree_to_dataframe, tree_to_polars # noqa
from .dictionaries import tree_to_dict, tree_to_nested_dict # noqa
from .dictionaries import ( # noqa
tree_to_dict,
tree_to_nested_dict,
tree_to_nested_dict_key,
)
from .images import ( # noqa
tree_to_dot,
tree_to_mermaid,
Expand All @@ -22,6 +26,7 @@
"tree_to_polars",
"tree_to_dict",
"tree_to_nested_dict",
"tree_to_nested_dict_key",
"tree_to_dot",
"tree_to_mermaid",
"tree_to_pillow",
Expand Down
69 changes: 69 additions & 0 deletions bigtree/tree/export/dictionaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
__all__ = [
"tree_to_dict",
"tree_to_nested_dict",
"tree_to_nested_dict_key",
]

T = TypeVar("T", bound=node.Node)
Expand Down Expand Up @@ -165,3 +166,71 @@ def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:

_recursive_append(tree, data_dict)
return data_dict[child_key][0]


def tree_to_nested_dict_key(
tree: T,
child_key: str = "children",
attr_dict: Optional[Dict[str, str]] = None,
all_attrs: bool = False,
max_depth: int = 0,
) -> Dict[str, Any]:
"""Export tree to nested dictionary, where the keys are node names.

All descendants from `tree` will be exported, `tree` can be the root node or child node of tree.

Exported dictionary will have key as node names, and children as node attributes and nested recursive dictionary.

Examples:
>>> from bigtree import Node, tree_to_nested_dict_key
>>> 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)
>>> tree_to_nested_dict_key(root, all_attrs=True)
{'a': {'age': 90, 'children': {'b': {'age': 65, 'children': {'d': {'age': 40}, 'e': {'age': 35}}}, 'c': {'age': 60}}}}

Args:
tree: tree to be exported
child_key: dictionary key for children
attr_dict: node attributes mapped to dictionary key, key: node attributes, value: corresponding dictionary key
all_attrs: indicator whether to retrieve all ``Node`` attributes, overrides `attr_dict`
max_depth: maximum depth to export tree

Returns:
Dictionary containing tree information
"""
data_dict: Dict[str, Dict[str, Any]] = {}

def _recursive_append(_node: T, parent_dict: Dict[str, Any]) -> None:
"""Recursively iterate through node and its children to export to nested dictionary.

Args:
_node: current node
parent_dict: parent dictionary
"""
if _node:
if not max_depth or _node.depth <= max_depth:
data_child = {}
if all_attrs:
data_child.update(
dict(
_node.describe(
exclude_attributes=["name"], exclude_prefix="_"
)
)
)
elif attr_dict:
for k, v in attr_dict.items():
data_child[v] = _node.get_attr(k)
if child_key in parent_dict:
parent_dict[child_key][_node.node_name] = data_child
else:
parent_dict[child_key] = {_node.node_name: data_child}

for _child in _node.children:
_recursive_append(_child, data_child)

_recursive_append(tree, data_dict)
return data_dict[child_key]
17 changes: 17 additions & 0 deletions bigtree/utils/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,23 @@ def assert_length_not_empty(
)


def assert_length(
data: Collection[Any], length: int, argument_name: str, argument: str
) -> None:
"""Raise ValueError if data does not have specific length.

Args:
data: data to check
length: length to check
argument_name: argument name for data, for error message
argument: argument for data, for error message
"""
if len(data) != length:
raise ValueError(
f"{argument_name} is not of length {length}, check `{argument}`"
)


def assert_dataframe_not_empty(data: pd.DataFrame) -> None:
"""Raise ValueError is dataframe is empty.

Expand Down
16 changes: 8 additions & 8 deletions docs/bigtree/tree/construct.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ Construct Tree from list, dictionary, and pandas DataFrame.

To decide which method to use, consider your data type and data values.

| Construct tree from | Using full path | Using parent-child relation | Using notation | Add node attributes |
|---------------------|---------------------|---------------------------------|------------------|---------------------------------------------------------|
| String | `str_to_tree` | NA | `newick_to_tree` | No (for ` str_to_tree `)<br>Yes (for `newick_to_tree`) |
| List | `list_to_tree` | ` list_to_tree_by_relation` | NA | No |
| Dictionary | `dict_to_tree` | ` nested_dict_to_tree` | NA | Yes |
| pandas DataFrame | `dataframe_to_tree` | `dataframe_to_tree_by_relation` | NA | Yes |
| polars DataFrame | `polars_to_tree` | `polars_to_tree_by_relation` | NA | Yes |
| Interactive UI | NA | `render_tree` | NA | No |
| Construct tree from | Using full path | Using parent-child relation | Using notation | Add node attributes |
|---------------------|---------------------|--------------------------------------------------|------------------|---------------------------------------------------------|
| String | `str_to_tree` | NA | `newick_to_tree` | No (for ` str_to_tree `)<br>Yes (for `newick_to_tree`) |
| List | `list_to_tree` | `list_to_tree_by_relation` | NA | No |
| Dictionary | `dict_to_tree` | `nested_dict_to_tree`, `nested_dict_key_to_tree` | NA | Yes |
| pandas DataFrame | `dataframe_to_tree` | `dataframe_to_tree_by_relation` | NA | Yes |
| polars DataFrame | `polars_to_tree` | `polars_to_tree_by_relation` | NA | Yes |
| Interactive UI | NA | `render_tree` | NA | No |

## Tree Add Attributes Methods

Expand Down
Loading