Skip to content

Commit 6ab3adf

Browse files
authored
Merge pull request #291 from kayjan/feat/plot-tree
Add plotting for tree
2 parents d2304cc + caa9731 commit 6ab3adf

File tree

18 files changed

+216
-26
lines changed

18 files changed

+216
-26
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [0.21.0] - TBD
10+
### Added:
11+
- Tree Plot: Plot tree using matplotlib library, added matplotlib as optional dependency.
12+
- BaseNode: Add plot method.
13+
### Changed:
14+
- Misc: Optional dependencies imported as MagicMock
15+
916
## [0.20.1] - 2024-08-24
1017
### Changed:
1118
- Misc: Documentation update contributing instructions.
@@ -638,7 +645,8 @@ ignore null attribute columns.
638645
- Utility Iterator: Tree traversal methods.
639646
- Workflow To Do App: Tree use case with to-do list implementation.
640647

641-
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.20.1...HEAD
648+
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.21.0...HEAD
649+
[0.21.0]: https://github.com/kayjan/bigtree/compare/0.20.1...0.21.0
642650
[0.20.1]: https://github.com/kayjan/bigtree/compare/0.20.0...0.20.1
643651
[0.20.0]: https://github.com/kayjan/bigtree/compare/0.19.4...0.20.0
644652
[0.19.4]: https://github.com/kayjan/bigtree/compare/0.19.3...0.19.4

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ For **Tree** implementation, there are 9 main components.
6767
4. Get difference between two trees
6868
7. [**📊 Plotting Tree**](https://bigtree.readthedocs.io/en/stable/bigtree/utils/plot/)
6969
1. Enhanced Reingold Tilford Algorithm to retrieve (x, y) coordinates for a tree structure
70+
2. Plot tree using matplotlib (optional dependency)
7071
8. [**🔨 Exporting Tree**](https://bigtree.readthedocs.io/en/stable/bigtree/tree/export/)
7172
1. Print to console, in vertical or horizontal orientation
7273
2. Export to *Newick string notation*, *dictionary*, *nested dictionary*, *pandas DataFrame*, or *polars DataFrame*

bigtree/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,6 @@
9393
zigzag_iter,
9494
zigzaggroup_iter,
9595
)
96-
from bigtree.utils.plot import reingold_tilford
96+
from bigtree.utils.plot import plot_tree, reingold_tilford
9797
from bigtree.workflows.app_calendar import Calendar
9898
from bigtree.workflows.app_todo import AppToDo

bigtree/dag/construct.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
try:
1717
import pandas as pd
1818
except ImportError: # pragma: no cover
19-
pd = None
19+
from unittest.mock import MagicMock
20+
21+
pd = MagicMock()
2022

2123
__all__ = ["list_to_dag", "dict_to_dag", "dataframe_to_dag"]
2224

bigtree/dag/export.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313
try:
1414
import pandas as pd
1515
except ImportError: # pragma: no cover
16-
pd = None
16+
from unittest.mock import MagicMock
17+
18+
pd = MagicMock()
1719

1820
try:
1921
import pydot
2022
except ImportError: # pragma: no cover
21-
pydot = None
23+
from unittest.mock import MagicMock
24+
25+
pydot = MagicMock()
2226

2327
__all__ = ["dag_to_list", "dag_to_dict", "dag_to_dataframe", "dag_to_dot"]
2428

bigtree/node/basenode.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
from bigtree.utils.exceptions import CorruptedTreeError, LoopError, TreeError
99
from bigtree.utils.iterators import preorder_iter
1010

11+
try:
12+
import matplotlib.pyplot as plt
13+
except ImportError: # pragma: no cover
14+
plt = None
15+
1116

1217
class BaseNode:
1318
"""
@@ -115,6 +120,7 @@ class BaseNode:
115120
6. ``extend(nodes: List[Self])``: Add multiple children to node
116121
7. ``copy()``: Deep copy self
117122
8. ``sort()``: Sort child nodes
123+
9. ``plot()``: Plot tree in line form
118124
119125
----
120126
@@ -727,6 +733,7 @@ def copy(self: T) -> T:
727733

728734
def sort(self: T, **kwargs: Any) -> None:
729735
"""Sort children, possible keyword arguments include ``key=lambda node: node.name``, ``reverse=True``
736+
Accepts kwargs for sort() function.
730737
731738
Examples:
732739
>>> from bigtree import Node, print_tree
@@ -747,6 +754,24 @@ def sort(self: T, **kwargs: Any) -> None:
747754
children.sort(**kwargs)
748755
self.__children = children
749756

757+
def plot(self, *args: Any, **kwargs: Any) -> plt.Figure:
758+
"""Plot tree in line form.
759+
Accepts args and kwargs for matplotlib.pyplot.plot() function.
760+
761+
Examples:
762+
>>> import matplotlib.pyplot as plt
763+
>>> from bigtree import list_to_tree
764+
>>> path_list = ["a/b/d", "a/b/e/g", "a/b/e/h", "a/c/f"]
765+
>>> root = list_to_tree(path_list)
766+
>>> root.plot("-ok")
767+
<Figure size 1280x960 with 1 Axes>
768+
"""
769+
from bigtree.utils.plot import plot_tree, reingold_tilford
770+
771+
if self.get_attr("x") is None or self.get_attr("y") is None:
772+
reingold_tilford(self)
773+
return plot_tree(self, *args, **kwargs)
774+
750775
def __copy__(self: T) -> T:
751776
"""Shallow copy self
752777

bigtree/tree/construct.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@
2424
try:
2525
import pandas as pd
2626
except ImportError: # pragma: no cover
27-
pd = None
27+
from unittest.mock import MagicMock
28+
29+
pd = MagicMock()
2830

2931
try:
3032
import polars as pl
3133
except ImportError: # pragma: no cover
32-
pl = None
34+
from unittest.mock import MagicMock
35+
36+
pl = MagicMock()
3337

3438
__all__ = [
3539
"add_path_to_tree",

bigtree/tree/export.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,30 @@
2828
try:
2929
import pandas as pd
3030
except ImportError: # pragma: no cover
31-
pd = None
31+
from unittest.mock import MagicMock
32+
33+
pd = MagicMock()
3234

3335
try:
3436
import polars as pl
3537
except ImportError: # pragma: no cover
36-
pl = None
38+
from unittest.mock import MagicMock
39+
40+
pl = MagicMock()
3741

3842
try:
3943
import pydot
4044
except ImportError: # pragma: no cover
41-
pydot = None
45+
from unittest.mock import MagicMock
46+
47+
pydot = MagicMock()
4248

4349
try:
4450
from PIL import Image, ImageDraw, ImageFont
4551
except ImportError: # pragma: no cover
46-
Image = ImageDraw = ImageFont = None
52+
from unittest.mock import MagicMock
53+
54+
Image = ImageDraw = ImageFont = MagicMock()
4755

4856

4957
__all__ = [
@@ -73,9 +81,10 @@ def print_tree(
7381
attr_omit_null: bool = False,
7482
attr_bracket: List[str] = ["[", "]"],
7583
style: Union[str, Iterable[str], BasePrintStyle] = "const",
76-
**print_kwargs: Any,
84+
**kwargs: Any,
7785
) -> None:
7886
"""Print tree to console, starting from `tree`.
87+
Accepts kwargs for print() function.
7988
8089
- Able to select which node to print from, resulting in a subtree, using `node_name_or_path`
8190
- Able to customize for maximum depth to print, using `max_depth`
@@ -91,8 +100,6 @@ def print_tree(
91100
- (BasePrintStyle): `ANSIPrintStyle`, `ASCIIPrintStyle`, `ConstPrintStyle`, `ConstBoldPrintStyle`, `RoundedPrintStyle`,
92101
`DoublePrintStyle` style or inherit from `BasePrintStyle`
93102
94-
Remaining kwargs are passed without modification to python's `print` function.
95-
96103
Examples:
97104
**Printing tree**
98105
@@ -249,7 +256,7 @@ def print_tree(
249256
if attr_str:
250257
attr_str = f" {attr_bracket_open}{attr_str}{attr_bracket_close}"
251258
node_str = f"{_node.node_name}{attr_str}"
252-
print(f"{pre_str}{fill_str}{node_str}", **print_kwargs)
259+
print(f"{pre_str}{fill_str}{node_str}", **kwargs)
253260

254261

255262
def yield_tree(
@@ -433,9 +440,10 @@ def hprint_tree(
433440
max_depth: int = 0,
434441
intermediate_node_name: bool = True,
435442
style: Union[str, Iterable[str], BaseHPrintStyle] = "const",
436-
**print_kwargs: Any,
443+
**kwargs: Any,
437444
) -> None:
438445
"""Print tree in horizontal orientation to console, starting from `tree`.
446+
Accepts kwargs for print() function.
439447
440448
- Able to select which node to print from, resulting in a subtree, using `node_name_or_path`
441449
- Able to customize for maximum depth to print, using `max_depth`
@@ -448,8 +456,6 @@ def hprint_tree(
448456
- (BaseHPrintStyle): `ANSIHPrintStyle`, `ASCIIHPrintStyle`, `ConstHPrintStyle`, `ConstBoldHPrintStyle`,
449457
`RoundedHPrintStyle`, `DoubleHPrintStyle` style or inherit from BaseHPrintStyle
450458
451-
Remaining kwargs are passed without modification to python's `print` function.
452-
453459
Examples:
454460
**Printing tree**
455461
@@ -549,7 +555,7 @@ def hprint_tree(
549555
max_depth=max_depth,
550556
style=style,
551557
)
552-
print("\n".join(result), **print_kwargs)
558+
print("\n".join(result), **kwargs)
553559

554560

555561
def hyield_tree(

bigtree/utils/assertions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
try:
66
import pandas as pd
77
except ImportError: # pragma: no cover
8-
pd = None
8+
from unittest.mock import MagicMock
9+
10+
pd = MagicMock()
911

1012
try:
1113
import polars as pl
1214
except ImportError: # pragma: no cover
13-
pl = None
15+
from unittest.mock import MagicMock
16+
17+
pl = MagicMock()
1418

1519

1620
if TYPE_CHECKING:

bigtree/utils/exceptions.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,28 @@ def wrapper(*args: Any, **kwargs: Any) -> T:
114114
return wrapper
115115

116116

117+
def optional_dependencies_matplotlib(
118+
func: Callable[..., T]
119+
) -> Callable[..., T]: # pragma: no cover
120+
"""
121+
This is a decorator which can be used to import optional matplotlib dependency.
122+
It will raise a ImportError if the module is not found.
123+
"""
124+
125+
@wraps(func)
126+
def wrapper(*args: Any, **kwargs: Any) -> T:
127+
try:
128+
import matplotlib.pyplot as plt # noqa: F401
129+
except ImportError:
130+
raise ImportError(
131+
"matplotlib not available. Please perform a\n\n"
132+
"pip install 'bigtree[matplotlib]'\n\nto install required dependencies"
133+
) from None
134+
return func(*args, **kwargs)
135+
136+
return wrapper
137+
138+
117139
def optional_dependencies_image(
118140
package_name: str = "",
119141
) -> Callable[[Callable[..., T]], Callable[..., T]]:

0 commit comments

Comments
 (0)