Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hprint to support multiline #353

Merged
merged 11 commits into from
Feb 28, 2025
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### Added:
- Tree Exporter: `vprint_tree` to have same arguments as `vyield_tree`, add more test cases.
- Tree Exporter: `hprint_tree` to support multiline node name, alias, and border style.

## [0.25.0] - 2025-02-25
### Added:
- Tree Exporter: `tree_to_pillow_graph` method to allow cmap for node background and generic kwargs for yield_tree.
- Tree Exporter: `vprint_tree` method to print trees vertically, able to use `.vshow()` as well.
### Changed:
Expand Down Expand Up @@ -734,7 +739,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.24.0...HEAD
[Unreleased]: https://github.com/kayjan/bigtree/compare/0.25.0...HEAD
[0.25.0]: https://github.com/kayjan/bigtree/compare/0.24.0...0.25.0
[0.24.0]: https://github.com/kayjan/bigtree/compare/0.23.1...0.24.0
[0.23.1]: https://github.com/kayjan/bigtree/compare/0.23.0...0.23.1
[0.23.0]: https://github.com/kayjan/bigtree/compare/0.22.3...0.23.0
Expand Down
2 changes: 1 addition & 1 deletion bigtree/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.24.0"
__version__ = "0.25.0"

from bigtree.binarytree.construct import list_to_binarytree
from bigtree.dag.construct import dataframe_to_dag, dict_to_dag, list_to_dag
Expand Down
114 changes: 88 additions & 26 deletions bigtree/tree/export/_stdout.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from __future__ import annotations

from typing import List, Optional, TypeVar
from typing import List, Optional, TypeVar, Union

from bigtree.node import node
from bigtree.utils.constants import BaseVPrintStyle, BorderStyle
from bigtree.utils.constants import BaseHPrintStyle, BaseVPrintStyle, BorderStyle

__all__ = [
"calculate_stem_pos",
"format_node",
"horizontal_join",
"vertical_join",
]

T = TypeVar("T", bound=node.Node)
Expand All @@ -30,20 +31,26 @@ def calculate_stem_pos(length: int) -> int:

def format_node(
_node: T,
alias: str,
alias: str = "node_name",
intermediate_node_name: bool = True,
style: BaseVPrintStyle = BaseVPrintStyle.from_style("const"),
style: Union[BaseHPrintStyle, BaseVPrintStyle] = BaseVPrintStyle.from_style(
"const"
),
border_style: Optional[BorderStyle] = None,
min_width: int = 0,
add_buffer: bool = True,
) -> List[str]:
"""Format node to be same width, able to customise whether to add border

Args:
_node (Node): node to format
alias (str): node attribute to use for node name in tree as alias to `node_name`, if present.
Otherwise, it will default to `node_name` of node.
alias (str): node attribute to use for node name in tree as alias to `node_name`. Otherwise, it will default to
`node_name` of node.
intermediate_node_name (bool): indicator if intermediate nodes have node names, defaults to True
style (BaseVPrintStyle): style to format node, used only if border_style is None
style (Union[BaseHPrintStyle, BaseVPrintStyle]): style to format node, used only if border_style is None
border_style (BorderStyle): border style to format node
min_width (int): minimum width of node display contents
add_buffer (bool): whether to add buffer if style is BaseHPrintStyle and border_style is None

Returns:
(List[str]) node display
Expand All @@ -52,13 +59,19 @@ def format_node(
if border_style is None:
node_title: str = style.BRANCH
if _node.is_root:
node_title = style.SUBSEQUENT_CHILD
if isinstance(style, BaseHPrintStyle):
node_title = style.BRANCH
else:
node_title = style.SUBSEQUENT_CHILD
else:
node_title = ""
else:
node_title = _node.get_attr(alias) or _node.node_name
node_title_lines = node_title.split("\n")
width = max([len(node_title_lines) for node_title_lines in node_title_lines])
width = max(
[len(node_title_lines) for node_title_lines in node_title_lines] + [min_width]
)
height = len(node_title_lines)

node_display_lines: List[str] = []
if border_style:
Expand All @@ -73,29 +86,59 @@ def format_node(
node_display_lines.append(
f"{border_style.BOTTOM_LEFT}{border_style.HORIZONTAL * width}{border_style.BOTTOM_RIGHT}"
)
node_mid = calculate_stem_pos(width + 2)
# If there is a parent
if _node.parent:
node_display_lines[0] = (
node_display_lines[0][:node_mid]
+ style.SPLIT_BRANCH
+ node_display_lines[0][node_mid + 1 :] # noqa
)
# If there are subsequent children
if any(_node.children):
node_display_lines[-1] = (
node_display_lines[-1][:node_mid]
+ style.SUBSEQUENT_CHILD
+ node_display_lines[-1][node_mid + 1 :] # noqa
)

if isinstance(style, BaseHPrintStyle):
node_mid = calculate_stem_pos(height + 2)
# If there is a parent
if _node.parent:
node_display_lines[node_mid] = (
style.SPLIT_BRANCH + node_display_lines[node_mid][1:]
)
# If there are subsequent children
if any(_node.children):
node_display_lines[node_mid] = (
node_display_lines[node_mid][:-1] + style.SUBSEQUENT_CHILD
)
else:
node_mid = calculate_stem_pos(width + 2)
# If there is a parent
if _node.parent:
node_display_lines[0] = (
node_display_lines[0][:node_mid]
+ style.SPLIT_BRANCH
+ node_display_lines[0][node_mid + 1 :] # noqa
)
# If there are subsequent children
if any(_node.children):
node_display_lines[-1] = (
node_display_lines[-1][:node_mid]
+ style.SUBSEQUENT_CHILD
+ node_display_lines[-1][node_mid + 1 :] # noqa
)
else:
for node_title_line in node_title_lines:
node_display_lines.append(f"{node_title_line.center(width)}")
if isinstance(style, BaseHPrintStyle) and add_buffer:
node_mid = calculate_stem_pos(height)
prefix_line = [" "] * height
prefix_line[node_mid] = style.BRANCH
prefix_line2 = [" "] * height
if intermediate_node_name or _node.is_leaf:
node_display_lines = horizontal_join(
[prefix_line, prefix_line2, node_display_lines]
)
else:
prefix_line2 = prefix_line
# If there are subsequent children
if any(_node.children):
node_display_lines = horizontal_join(
[node_display_lines, prefix_line2, prefix_line]
)
return node_display_lines


def horizontal_join(node_displays: List[List[str]], spacing: int = 2) -> List[str]:
"""Horizontally join multiple node displays
def horizontal_join(node_displays: List[List[str]], spacing: int = 0) -> List[str]:
"""Horizontally join multiple node displays, for displaying tree vertically

Args:
node_displays (List[List[str]]): multiple node displays belonging to the same row
Expand All @@ -122,3 +165,22 @@ def horizontal_join(node_displays: List[List[str]], spacing: int = 2) -> List[st
row_display[row_idx_buffer] += spacing * space
row_display[row_idx_buffer] += width * space
return [row_display[k] for k in sorted(row_display)]


def vertical_join(node_displays: List[List[str]]) -> List[str]:
"""Vertically join multiple node displays, for displaying tree horizontally

Args:
node_displays (List[List[str]]): multiple node displays belonging to the same column

Returns:
(List[str]) node display of the row
"""
space = " "
width = max([len(node_display[0]) for node_display in node_displays])
rows_display = []
for node_display in node_displays:
for node_display_row in node_display:
spacing = width - len(node_display_row)
rows_display.append(node_display_row + spacing * space)
return rows_display
Loading