diff --git a/CHANGELOG.md b/CHANGELOG.md index 5500a5a9..9c3c37a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +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] +### Added: +- Tree Export: Mermaid diagram to include theme. +### Fixed: +- Misc: Doctest for docstrings, docstring to indicate usage prefers `node_name` to `name`. +- Tree Export: Mermaid diagram title to add newline. ## [0.22.1] - 2024-11-03 ### Added: diff --git a/bigtree/node/node.py b/bigtree/node/node.py index 785fac00..f0a0a874 100644 --- a/bigtree/node/node.py +++ b/bigtree/node/node.py @@ -61,7 +61,9 @@ class Node(basenode.BaseNode): Get `Node` configuration - 1. ``node_name``: Get node name, without accessing `name` directly + 1. ``node_name``: Get node name, without accessing `name` directly. + This is the preferred way to access node name as `node_name` is + immutable, whereas `name` is mutable. 2. ``path_name``: Get path name from root, separated by `sep` **Node Methods** diff --git a/bigtree/tree/export.py b/bigtree/tree/export.py index db505275..baead46c 100644 --- a/bigtree/tree/export.py +++ b/bigtree/tree/export.py @@ -1430,6 +1430,7 @@ def get_list_of_text_dimensions( def tree_to_mermaid( tree: T, title: str = "", + theme: Optional[str] = None, rankdir: str = "TB", line_shape: str = "basis", node_colour: str = "", @@ -1447,6 +1448,7 @@ def tree_to_mermaid( Parameters for customizations that apply to entire flowchart include: - Title, `title` + - Theme, `theme` - Layout direction, `rankdir` - Line shape or curvature, `line_shape` - Fill colour of nodes, `node_colour` @@ -1465,6 +1467,13 @@ def tree_to_mermaid( **Accepted Parameter Values** + Possible theme: + - default + - neutral: great for black and white documents + - dark: great for dark-mode + - forest: shades of geen + - base: theme that can be modified, use it for customizations + Possible rankdir: - `TB`: top-to-bottom - `BT`: bottom-to-top @@ -1546,6 +1555,7 @@ def tree_to_mermaid( >>> graph = tree_to_mermaid( ... root, ... title="Mermaid Diagram", + ... theme="forest", ... node_shape_attr="node_shape", ... edge_label="edge_label", ... edge_arrow_attr="edge_arrow", @@ -1556,7 +1566,7 @@ def tree_to_mermaid( --- title: Mermaid Diagram --- - %%{ init: { 'flowchart': { 'curve': 'basis' } } }%% + %%{ init: { 'flowchart': { 'curve': 'basis' }, 'theme': 'forest' } }%% flowchart TB 0{"a"} ==>|Child 1| 0-0("b") 0-0 --> 0-0-0("d"):::class0-0-0 @@ -1593,12 +1603,15 @@ def tree_to_mermaid( """ from bigtree.tree.helper import clone_tree + themes = constants.MermaidConstants.THEMES rankdirs = constants.MermaidConstants.RANK_DIR line_shapes = constants.MermaidConstants.LINE_SHAPES node_shapes = constants.MermaidConstants.NODE_SHAPES edge_arrows = constants.MermaidConstants.EDGE_ARROWS # Assertions + if theme: + assertions.assert_str_in_list("theme", theme, themes) assertions.assert_str_in_list("rankdir", rankdir, rankdirs) assertions.assert_key_in_dict("node_shape", node_shape, node_shapes) assertions.assert_str_in_list("line_shape", line_shape, line_shapes) @@ -1609,8 +1622,9 @@ def tree_to_mermaid( style_template = "classDef {style_name} {style}" # Content + theme_mermaid = f", 'theme': '{theme}'" if theme else "" title = f"---\ntitle: {title}\n---\n" if title else "" - line_style = f"%%{{ init: {{ 'flowchart': {{ 'curve': '{line_shape}' }} }} }}%%" + line_style = f"%%{{ init: {{ 'flowchart': {{ 'curve': '{line_shape}' }}{theme_mermaid} }} }}%%" styles = [] flows = [] diff --git a/bigtree/utils/constants.py b/bigtree/utils/constants.py index 63e099ca..8c46a305 100644 --- a/bigtree/utils/constants.py +++ b/bigtree/utils/constants.py @@ -171,6 +171,7 @@ def __post_init__(self) -> None: class MermaidConstants: + THEMES: List[str] = ["default", "neutral", "dark", "forest", "base"] RANK_DIR: List[str] = ["TB", "BT", "LR", "RL"] LINE_SHAPES: List[str] = [ "basis", diff --git a/tests/tree/test_export.py b/tests/tree/test_export.py index 0911f3d4..8d12e1ec 100644 --- a/tests/tree/test_export.py +++ b/tests/tree/test_export.py @@ -1653,7 +1653,26 @@ class TestTreeToDot: @staticmethod def test_tree_to_dot(tree_node): graph = export.tree_to_dot(tree_node) - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -> b0;\nd0 [label=d];\nb0 -> d0;\ne0 [label=e];\nb0 -> e0;\ng0 [label=g];\ne0 -> g0;\nh0 [label=h];\ne0 -> h0;\nc0 [label=c];\na0 -> c0;\nf0 [label=f];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "b0 [label=b];\n" + "a0 -> b0;\n" + "d0 [label=d];\n" + "b0 -> d0;\n" + "e0 [label=e];\n" + "b0 -> e0;\n" + "g0 [label=g];\n" + "e0 -> g0;\n" + "h0 [label=h];\n" + "e0 -> h0;\n" + "c0 [label=c];\n" + "a0 -> c0;\n" + "f0 [label=f];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree.png") @@ -1665,7 +1684,29 @@ def test_tree_to_dot(tree_node): @staticmethod def test_tree_to_dot_multiple(tree_node, tree_node_plot): graph = export.tree_to_dot([tree_node, tree_node_plot]) - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -> b0;\nd0 [label=d];\nb0 -> d0;\ne0 [label=e];\nb0 -> e0;\ng0 [label=g];\ne0 -> g0;\nh0 [label=h];\ne0 -> h0;\nc0 [label=c];\na0 -> c0;\nf0 [label=f];\nc0 -> f0;\nz0 [label=z];\ny0 [label=y];\nz0 -> y0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "b0 [label=b];\n" + "a0 -> b0;\n" + "d0 [label=d];\n" + "b0 -> d0;\n" + "e0 [label=e];\n" + "b0 -> e0;\n" + "g0 [label=g];\n" + "e0 -> g0;\n" + "h0 [label=h];\n" + "e0 -> h0;\n" + "c0 [label=c];\n" + "a0 -> c0;\n" + "f0 [label=f];\n" + "c0 -> f0;\n" + "z0 [label=z];\n" + "y0 [label=y];\n" + "z0 -> y0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_multiple.png") @@ -1677,7 +1718,26 @@ def test_tree_to_dot_multiple(tree_node, tree_node_plot): @staticmethod def test_tree_to_dot_duplicate_names(tree_node_duplicate_names): graph = export.tree_to_dot(tree_node_duplicate_names) - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\na1 [label=a];\na0 -> a1;\na2 [label=a];\na1 -> a2;\nb0 [label=b];\na1 -> b0;\na3 [label=a];\nb0 -> a3;\nb1 [label=b];\nb0 -> b1;\nb2 [label=b];\na0 -> b2;\na4 [label=a];\nb2 -> a4;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "a1 [label=a];\n" + "a0 -> a1;\n" + "a2 [label=a];\n" + "a1 -> a2;\n" + "b0 [label=b];\n" + "a1 -> b0;\n" + "a3 [label=a];\n" + "b0 -> a3;\n" + "b1 [label=b];\n" + "b0 -> b1;\n" + "b2 [label=b];\n" + "a0 -> b2;\n" + "a4 [label=a];\n" + "b2 -> a4;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_duplicate_names.png") @@ -1695,7 +1755,26 @@ def test_tree_to_dot_type_error(dag_node): @staticmethod def test_tree_to_dot_directed(tree_node): graph = export.tree_to_dot(tree_node, directed=False) - expected = """strict graph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -- b0;\nd0 [label=d];\nb0 -- d0;\ne0 [label=e];\nb0 -- e0;\ng0 [label=g];\ne0 -- g0;\nh0 [label=h];\ne0 -- h0;\nc0 [label=c];\na0 -- c0;\nf0 [label=f];\nc0 -- f0;\n}\n""" + expected = ( + "strict graph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "b0 [label=b];\n" + "a0 -- b0;\n" + "d0 [label=d];\n" + "b0 -- d0;\n" + "e0 [label=e];\n" + "b0 -- e0;\n" + "g0 [label=g];\n" + "e0 -- g0;\n" + "h0 [label=h];\n" + "e0 -- h0;\n" + "c0 [label=c];\n" + "a0 -- c0;\n" + "f0 [label=f];\n" + "c0 -- f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_undirected.png") @@ -1707,7 +1786,26 @@ def test_tree_to_dot_directed(tree_node): @staticmethod def test_tree_to_dot_bg_colour(tree_node): graph = export.tree_to_dot(tree_node, bg_colour="blue") - expected = """strict digraph G {\nbgcolor=blue;\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -> b0;\nd0 [label=d];\nb0 -> d0;\ne0 [label=e];\nb0 -> e0;\ng0 [label=g];\ne0 -> g0;\nh0 [label=h];\ne0 -> h0;\nc0 [label=c];\na0 -> c0;\nf0 [label=f];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "bgcolor=blue;\n" + "rankdir=TB;\na0 [label=a];\n" + "b0 [label=b];\n" + "a0 -> b0;\n" + "d0 [label=d];\n" + "b0 -> d0;\n" + "e0 [label=e];\n" + "b0 -> e0;\n" + "g0 [label=g];\n" + "e0 -> g0;\n" + "h0 [label=h];\n" + "e0 -> h0;\n" + "c0 [label=c];\n" + "a0 -> c0;\n" + "f0 [label=f];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_bg_colour.png") @@ -1722,7 +1820,26 @@ def test_tree_to_dot_bg_colour(tree_node): ) def test_tree_to_dot_fill_colour(tree_node): graph = export.tree_to_dot(tree_node, node_colour="gold") - expected = """strict digraph G {\nrankdir=TB;\na0 [fillcolor=gold, label=a, style=filled];\nb0 [fillcolor=gold, label=b, style=filled];\na0 -> b0;\nd0 [fillcolor=gold, label=d, style=filled];\nb0 -> d0;\ne0 [fillcolor=gold, label=e, style=filled];\nb0 -> e0;\ng0 [fillcolor=gold, label=g, style=filled];\ne0 -> g0;\nh0 [fillcolor=gold, label=h, style=filled];\ne0 -> h0;\nc0 [fillcolor=gold, label=c, style=filled];\na0 -> c0;\nf0 [fillcolor=gold, label=f, style=filled];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [fillcolor=gold, label=a, style=filled];\n" + "b0 [fillcolor=gold, label=b, style=filled];\n" + "a0 -> b0;\n" + "d0 [fillcolor=gold, label=d, style=filled];\n" + "b0 -> d0;\n" + "e0 [fillcolor=gold, label=e, style=filled];\n" + "b0 -> e0;\n" + "g0 [fillcolor=gold, label=g, style=filled];\n" + "e0 -> g0;\n" + "h0 [fillcolor=gold, label=h, style=filled];\n" + "e0 -> h0;\n" + "c0 [fillcolor=gold, label=c, style=filled];\n" + "a0 -> c0;\n" + "f0 [fillcolor=gold, label=f, style=filled];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_fill_colour.png") @@ -1738,7 +1855,26 @@ def test_tree_to_dot_fill_colour(tree_node): ) def test_tree_to_dot_fill_colour2(tree_node): graph = export.tree_to_dot(tree_node, node_colour="gold") - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a, style=filled, fillcolor=gold];\nb0 [label=b, style=filled, fillcolor=gold];\na0 -> b0;\nd0 [label=d, style=filled, fillcolor=gold];\nb0 -> d0;\ne0 [label=e, style=filled, fillcolor=gold];\nb0 -> e0;\ng0 [label=g, style=filled, fillcolor=gold];\ne0 -> g0;\nh0 [label=h, style=filled, fillcolor=gold];\ne0 -> h0;\nc0 [label=c, style=filled, fillcolor=gold];\na0 -> c0;\nf0 [label=f, style=filled, fillcolor=gold];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a, style=filled, fillcolor=gold];\n" + "b0 [label=b, style=filled, fillcolor=gold];\n" + "a0 -> b0;\n" + "d0 [label=d, style=filled, fillcolor=gold];\n" + "b0 -> d0;\n" + "e0 [label=e, style=filled, fillcolor=gold];\n" + "b0 -> e0;\n" + "g0 [label=g, style=filled, fillcolor=gold];\n" + "e0 -> g0;\n" + "h0 [label=h, style=filled, fillcolor=gold];\n" + "e0 -> h0;\n" + "c0 [label=c, style=filled, fillcolor=gold];\n" + "a0 -> c0;\n" + "f0 [label=f, style=filled, fillcolor=gold];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_fill_colour.png") @@ -1750,7 +1886,26 @@ def test_tree_to_dot_fill_colour2(tree_node): @staticmethod def test_tree_to_dot_edge_colour(tree_node): graph = export.tree_to_dot(tree_node, edge_colour="red") - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -> b0 [color=red];\nd0 [label=d];\nb0 -> d0 [color=red];\ne0 [label=e];\nb0 -> e0 [color=red];\ng0 [label=g];\ne0 -> g0 [color=red];\nh0 [label=h];\ne0 -> h0 [color=red];\nc0 [label=c];\na0 -> c0 [color=red];\nf0 [label=f];\nc0 -> f0 [color=red];\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "b0 [label=b];\n" + "a0 -> b0 [color=red];\n" + "d0 [label=d];\n" + "b0 -> d0 [color=red];\n" + "e0 [label=e];\n" + "b0 -> e0 [color=red];\n" + "g0 [label=g];\n" + "e0 -> g0 [color=red];\n" + "h0 [label=h];\n" + "e0 -> h0 [color=red];\n" + "c0 [label=c];\n" + "a0 -> c0 [color=red];\n" + "f0 [label=f];\n" + "c0 -> f0 [color=red];\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_edge_colour.png") @@ -1762,7 +1917,22 @@ def test_tree_to_dot_edge_colour(tree_node): @staticmethod def test_tree_to_dot_node_shape(tree_node): graph = export.tree_to_dot(tree_node, node_shape="triangle") - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a, shape=triangle];\nb0 [label=b, shape=triangle];\na0 -> b0;\nd0 [label=d, shape=triangle];\nb0 -> d0;\ne0 [label=e, shape=triangle];\nb0 -> e0;\ng0 [label=g, shape=triangle];\ne0 -> g0;\nh0 [label=h, shape=triangle];\ne0 -> h0;\nc0 [label=c, shape=triangle];\na0 -> c0;\nf0 [label=f, shape=triangle];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a, shape=triangle];\nb0 [label=b, shape=triangle];\n" + "a0 -> b0;\n" + "d0 [label=d, shape=triangle];\n" + "b0 -> d0;\ne0 [label=e, shape=triangle];\n" + "b0 -> e0;\n" + "g0 [label=g, shape=triangle];\n" + "e0 -> g0;\nh0 [label=h, shape=triangle];\n" + "e0 -> h0;\n" + "c0 [label=c, shape=triangle];\n" + "a0 -> c0;\nf0 [label=f, shape=triangle];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_node_shape.png") @@ -1777,7 +1947,25 @@ def test_tree_to_dot_node_shape(tree_node): ) def test_tree_to_dot_node_attr(tree_node_style): graph = export.tree_to_dot(tree_node_style, node_attr="node_style") - expected = """strict digraph G {\nrankdir=TB;\na0 [fillcolor=gold, label=a, style=filled];\nb0 [fillcolor=blue, label=b, style=filled];\na0 -> b0;\nd0 [fillcolor=green, label=d, style=filled];\nb0 -> d0;\ng0 [fillcolor=red, label=g, style=filled];\nd0 -> g0;\ne0 [fillcolor=green, label=e, style=filled];\nb0 -> e0;\nh0 [fillcolor=red, label=h, style=filled];\ne0 -> h0;\nc0 [fillcolor=blue, label=c, style=filled];\na0 -> c0;\nf0 [fillcolor=green, label=f, style=filled];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [fillcolor=gold, label=a, style=filled];\n" + "b0 [fillcolor=blue, label=b, style=filled];\na0 -> b0;\n" + "d0 [fillcolor=green, label=d, style=filled];\n" + "b0 -> d0;\n" + "g0 [fillcolor=red, label=g, style=filled];\n" + "d0 -> g0;\n" + "e0 [fillcolor=green, label=e, style=filled];\n" + "b0 -> e0;\n" + "h0 [fillcolor=red, label=h, style=filled];\n" + "e0 -> h0;\n" + "c0 [fillcolor=blue, label=c, style=filled];\n" + "a0 -> c0;\n" + "f0 [fillcolor=green, label=f, style=filled];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_node_attr.png") @@ -1793,7 +1981,26 @@ def test_tree_to_dot_node_attr(tree_node_style): ) def test_tree_to_dot_node_attr2(tree_node_style): graph = export.tree_to_dot(tree_node_style, node_attr="node_style") - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a, style=filled, fillcolor=gold];\nb0 [label=b, style=filled, fillcolor=blue];\na0 -> b0;\nd0 [label=d, style=filled, fillcolor=green];\nb0 -> d0;\ng0 [label=g, style=filled, fillcolor=red];\nd0 -> g0;\ne0 [label=e, style=filled, fillcolor=green];\nb0 -> e0;\nh0 [label=h, style=filled, fillcolor=red];\ne0 -> h0;\nc0 [label=c, style=filled, fillcolor=blue];\na0 -> c0;\nf0 [label=f, style=filled, fillcolor=green];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a, style=filled, fillcolor=gold];\n" + "b0 [label=b, style=filled, fillcolor=blue];\n" + "a0 -> b0;\n" + "d0 [label=d, style=filled, fillcolor=green];\n" + "b0 -> d0;\n" + "g0 [label=g, style=filled, fillcolor=red];\n" + "d0 -> g0;\n" + "e0 [label=e, style=filled, fillcolor=green];\n" + "b0 -> e0;\n" + "h0 [label=h, style=filled, fillcolor=red];\n" + "e0 -> h0;\n" + "c0 [label=c, style=filled, fillcolor=blue];\n" + "a0 -> c0;\n" + "f0 [label=f, style=filled, fillcolor=green];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_node_attr.png") @@ -1817,7 +2024,26 @@ def get_node_attr(node): return {"style": "filled", "fillcolor": "red"} graph = export.tree_to_dot(tree_node_style_callable, node_attr=get_node_attr) - expected = """strict digraph G {\nrankdir=TB;\na0 [fillcolor=gold, label=a, style=filled];\nb0 [fillcolor=blue, label=b, style=filled];\na0 -> b0;\nd0 [fillcolor=green, label=d, style=filled];\nb0 -> d0;\ng0 [fillcolor=red, label=g, style=filled];\nd0 -> g0;\ne0 [fillcolor=green, label=e, style=filled];\nb0 -> e0;\nh0 [fillcolor=red, label=h, style=filled];\ne0 -> h0;\nc0 [fillcolor=blue, label=c, style=filled];\na0 -> c0;\nf0 [fillcolor=green, label=f, style=filled];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [fillcolor=gold, label=a, style=filled];\n" + "b0 [fillcolor=blue, label=b, style=filled];\n" + "a0 -> b0;\n" + "d0 [fillcolor=green, label=d, style=filled];\n" + "b0 -> d0;\n" + "g0 [fillcolor=red, label=g, style=filled];\n" + "d0 -> g0;\n" + "e0 [fillcolor=green, label=e, style=filled];\n" + "b0 -> e0;\n" + "h0 [fillcolor=red, label=h, style=filled];\n" + "e0 -> h0;\n" + "c0 [fillcolor=blue, label=c, style=filled];\n" + "a0 -> c0;\n" + "f0 [fillcolor=green, label=f, style=filled];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_node_attr_callable.png") @@ -1842,7 +2068,26 @@ def get_node_attr(node): return {"style": "filled", "fillcolor": "red"} graph = export.tree_to_dot(tree_node_style_callable, node_attr=get_node_attr) - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a, style=filled, fillcolor=gold];\nb0 [label=b, style=filled, fillcolor=blue];\na0 -> b0;\nd0 [label=d, style=filled, fillcolor=green];\nb0 -> d0;\ng0 [label=g, style=filled, fillcolor=red];\nd0 -> g0;\ne0 [label=e, style=filled, fillcolor=green];\nb0 -> e0;\nh0 [label=h, style=filled, fillcolor=red];\ne0 -> h0;\nc0 [label=c, style=filled, fillcolor=red];\na0 -> c0;\nf0 [label=f, style=filled, fillcolor=green];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a, style=filled, fillcolor=gold];\n" + "b0 [label=b, style=filled, fillcolor=blue];\n" + "a0 -> b0;\n" + "d0 [label=d, style=filled, fillcolor=green];\n" + "b0 -> d0;\n" + "g0 [label=g, style=filled, fillcolor=red];\n" + "d0 -> g0;\n" + "e0 [label=e, style=filled, fillcolor=green];\n" + "b0 -> e0;\n" + "h0 [label=h, style=filled, fillcolor=red];\n" + "e0 -> h0;\n" + "c0 [label=c, style=filled, fillcolor=red];\n" + "a0 -> c0;\n" + "f0 [label=f, style=filled, fillcolor=green];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_node_attr_callable.png") @@ -1857,7 +2102,26 @@ def get_node_attr(node): ) def test_tree_to_dot_edge_attr(tree_node_style): graph = export.tree_to_dot(tree_node_style, edge_attr="edge_style") - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -> b0 [label=b, style=bold];\nd0 [label=d];\nb0 -> d0 [label=1, style=bold];\ng0 [label=g];\nd0 -> g0 [label=4, style=bold];\ne0 [label=e];\nb0 -> e0 [label=2, style=bold];\nh0 [label=h];\ne0 -> h0 [label=5, style=bold];\nc0 [label=c];\na0 -> c0 [label=c, style=bold];\nf0 [label=f];\nc0 -> f0 [label=3, style=bold];\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "b0 [label=b];\n" + "a0 -> b0 [label=b, style=bold];\n" + "d0 [label=d];\n" + "b0 -> d0 [label=1, style=bold];\n" + "g0 [label=g];\n" + "d0 -> g0 [label=4, style=bold];\n" + "e0 [label=e];\n" + "b0 -> e0 [label=2, style=bold];\n" + "h0 [label=h];\n" + "e0 -> h0 [label=5, style=bold];\n" + "c0 [label=c];\n" + "a0 -> c0 [label=c, style=bold];\n" + "f0 [label=f];\n" + "c0 -> f0 [label=3, style=bold];\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_edge_attr.png") @@ -1873,7 +2137,26 @@ def test_tree_to_dot_edge_attr(tree_node_style): ) def test_tree_to_dot_edge_attr2(tree_node_style): graph = export.tree_to_dot(tree_node_style, edge_attr="edge_style") - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -> b0 [style=bold, label=b];\nd0 [label=d];\nb0 -> d0 [style=bold, label=1];\ng0 [label=g];\nd0 -> g0 [style=bold, label=4];\ne0 [label=e];\nb0 -> e0 [style=bold, label=2];\nh0 [label=h];\ne0 -> h0 [style=bold, label=5];\nc0 [label=c];\na0 -> c0 [style=bold, label=c];\nf0 [label=f];\nc0 -> f0 [style=bold, label=3];\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "b0 [label=b];\n" + "a0 -> b0 [style=bold, label=b];\n" + "d0 [label=d];\n" + "b0 -> d0 [style=bold, label=1];\n" + "g0 [label=g];\n" + "d0 -> g0 [style=bold, label=4];\n" + "e0 [label=e];\n" + "b0 -> e0 [style=bold, label=2];\n" + "h0 [label=h];\n" + "e0 -> h0 [style=bold, label=5];\n" + "c0 [label=c];\n" + "a0 -> c0 [style=bold, label=c];\n" + "f0 [label=f];\n" + "c0 -> f0 [style=bold, label=3];\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_edge_attr.png") @@ -1902,7 +2185,26 @@ def get_edge_attr(node): raise Exception("Node with invalid edge_attr not covered") graph = export.tree_to_dot(tree_node_style_callable, edge_attr=get_edge_attr) - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -> b0 [label=b, style=bold];\nd0 [label=d];\nb0 -> d0 [label=1, style=bold];\ng0 [label=g];\nd0 -> g0 [label=4, style=bold];\ne0 [label=e];\nb0 -> e0 [label=2, style=bold];\nh0 [label=h];\ne0 -> h0 [label=5, style=bold];\nc0 [label=c];\na0 -> c0 [label=c, style=bold];\nf0 [label=f];\nc0 -> f0 [label=3, style=bold];\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "b0 [label=b];\n" + "a0 -> b0 [label=b, style=bold];\n" + "d0 [label=d];\n" + "b0 -> d0 [label=1, style=bold];\n" + "g0 [label=g];\n" + "d0 -> g0 [label=4, style=bold];\n" + "e0 [label=e];\n" + "b0 -> e0 [label=2, style=bold];\n" + "h0 [label=h];\n" + "e0 -> h0 [label=5, style=bold];\n" + "c0 [label=c];\n" + "a0 -> c0 [label=c, style=bold];\n" + "f0 [label=f];\n" + "c0 -> f0 [label=3, style=bold];\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_edge_attr_callable.png") @@ -1932,7 +2234,25 @@ def get_edge_attr(node): raise Exception("Node with invalid edge_attr not covered") graph = export.tree_to_dot(tree_node_style_callable, edge_attr=get_edge_attr) - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b];\na0 -> b0 [style=bold, label=b];\nd0 [label=d];\nb0 -> d0 [style=bold, label=1];\ng0 [label=g];\nd0 -> g0 [style=bold, label=4];\ne0 [label=e];\nb0 -> e0 [style=bold, label=2];\nh0 [label=h];\ne0 -> h0 [style=bold, label=5];\nc0 [label=c];\na0 -> c0 [style=bold, label=c];\nf0 [label=f];\nc0 -> f0 [style=bold, label=3];\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\nb0 [label=b];\n" + "a0 -> b0 [style=bold, label=b];\n" + "d0 [label=d];\n" + "b0 -> d0 [style=bold, label=1];\n" + "g0 [label=g];\n" + "d0 -> g0 [style=bold, label=4];\n" + "e0 [label=e];\n" + "b0 -> e0 [style=bold, label=2];\n" + "h0 [label=h];\n" + "e0 -> h0 [style=bold, label=5];\n" + "c0 [label=c];\n" + "a0 -> c0 [style=bold, label=c];\n" + "f0 [label=f];\n" + "c0 -> f0 [style=bold, label=3];\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_edge_attr_callable.png") @@ -1955,7 +2275,26 @@ def test_tree_to_dot_attr_override(tree_node): graph = export.tree_to_dot( tree_node, node_attr="node_style", edge_attr="edge_style" ) - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [fillcolor=blue, label=b, style=filled];\na0 -> b0 [style=bold];\nd0 [label=d];\nb0 -> d0;\ne0 [label=e];\nb0 -> e0;\ng0 [label=g];\ne0 -> g0;\nh0 [label=h];\ne0 -> h0;\nc0 [label=c];\na0 -> c0;\nf0 [label=f];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "b0 [fillcolor=blue, label=b, style=filled];\n" + "a0 -> b0 [style=bold];\n" + "d0 [label=d];\n" + "b0 -> d0;\n" + "e0 [label=e];\n" + "b0 -> e0;\n" + "g0 [label=g];\n" + "e0 -> g0;\n" + "h0 [label=h];\n" + "e0 -> h0;\n" + "c0 [label=c];\n" + "a0 -> c0;\n" + "f0 [label=f];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_attr_override.png") @@ -1979,7 +2318,23 @@ def test_tree_to_dot_attr_override2(tree_node): graph = export.tree_to_dot( tree_node, node_attr="node_style", edge_attr="edge_style" ) - expected = """strict digraph G {\nrankdir=TB;\na0 [label=a];\nb0 [label=b, style=filled, fillcolor=blue];\na0 -> b0 [style=bold];\nd0 [label=d];\nb0 -> d0;\ne0 [label=e];\nb0 -> e0;\ng0 [label=g];\ne0 -> g0;\nh0 [label=h];\ne0 -> h0;\nc0 [label=c];\na0 -> c0;\nf0 [label=f];\nc0 -> f0;\n}\n""" + expected = ( + "strict digraph G {\n" + "rankdir=TB;\n" + "a0 [label=a];\n" + "b0 [label=b, style=filled, fillcolor=blue];\n" + "a0 -> b0 [style=bold];\n" + "d0 [label=d];\n" + "b0 -> d0;\n" + "e0 [label=e];\nb0 -> e0;\n" + "g0 [label=g];\n" + "e0 -> g0;\nh0 [label=h];\n" + "e0 -> h0;\n" + "c0 [label=c];\na0 -> c0;\n" + "f0 [label=f];\n" + "c0 -> f0;\n" + "}\n" + ) actual = graph.to_string() if LOCAL: graph.write_png("tests/tree_attr_override.png") @@ -2121,6 +2476,33 @@ def test_tree_to_mermaid_title(tree_node): ) assert mermaid_md == expected_str + @staticmethod + def test_tree_to_mermaid_theme(tree_node): + mermaid_md = export.tree_to_mermaid(tree_node, theme="forest") + expected_str = ( + """```mermaid\n""" + """%%{ init: { \'flowchart\': { \'curve\': \'basis\' }, \'theme\': \'forest\' } }%%\n""" + """flowchart TB\n""" + """0("a") --> 0-0("b")\n""" + """0-0 --> 0-0-0("d")\n""" + """0-0 --> 0-0-1("e")\n""" + """0-0-1 --> 0-0-1-0("g")\n""" + """0-0-1 --> 0-0-1-1("h")\n""" + """0("a") --> 0-1("c")\n""" + """0-1 --> 0-1-0("f")\n""" + """classDef default stroke-width:1\n""" + """```""" + ) + assert mermaid_md == expected_str + + @staticmethod + def test_tree_to_mermaid_invalid_theme_error(tree_node): + with pytest.raises(ValueError) as exc_info: + export.tree_to_mermaid(tree_node, theme="invalid") + assert str(exc_info.value).startswith( + Constants.ERROR_NODE_MERMAID_INVALID_ARGUMENT.format(parameter="theme") + ) + @staticmethod def test_tree_to_mermaid_invalid_rankdir_error(tree_node): with pytest.raises(ValueError) as exc_info: