|
1 | 1 | from .base import DataStream |
2 | 2 |
|
| 3 | +_ICON_MAP = { |
| 4 | + False: "📄", |
| 5 | + True: "📂", |
| 6 | + None: "❓", |
| 7 | +} |
| 8 | + |
| 9 | + |
| 10 | +def _get_node_icon(node: DataStream, show_missing_indicator: bool) -> str: |
| 11 | + node_icon = _ICON_MAP[node.is_collection] |
| 12 | + if not node.has_data and show_missing_indicator: |
| 13 | + node_icon += _ICON_MAP[None] |
| 14 | + return node_icon |
| 15 | + |
| 16 | + |
| 17 | +def _build_line_prefix(parents: list[bool], is_last: bool) -> str: |
| 18 | + line_prefix = "" |
| 19 | + for parent_is_last in parents[:-1]: |
| 20 | + line_prefix += " " if parent_is_last else "│ " |
| 21 | + if parents: |
| 22 | + branch = "└── " if is_last else "├── " |
| 23 | + line_prefix += branch |
| 24 | + return line_prefix |
| 25 | + |
| 26 | + |
| 27 | +def _build_node_label(node: DataStream, show_type: bool, show_params: bool) -> str: |
| 28 | + node_label = node.name |
| 29 | + if show_type: |
| 30 | + node_label += f" [{node.__class__.__name__}]" |
| 31 | + if show_params and hasattr(node, "reader_params") and node.reader_params: |
| 32 | + node_label += f" ({node.reader_params})" |
| 33 | + return node_label |
| 34 | + |
3 | 35 |
|
4 | 36 | def print_data_stream_tree( |
5 | 37 | node: DataStream, |
@@ -32,61 +64,202 @@ def print_data_stream_tree( |
32 | 64 | from contraqctor.contract import Dataset, csv, json |
33 | 65 | from contraqctor.contract.utils import print_data_stream_tree |
34 | 66 |
|
35 | | - # Create a dataset with streams |
36 | 67 | csv_stream = csv.Csv("data", reader_params=csv.CsvParams(path="data.csv")) |
37 | 68 | json_stream = json.Json("config", reader_params=json.JsonParams(path="config.json")) |
38 | 69 | dataset = Dataset("experiment", [csv_stream, json_stream], version="1.0.0") |
39 | 70 |
|
40 | | - # Print the tree |
41 | 71 | tree = print_data_stream_tree(dataset) |
42 | 72 | print(tree) |
43 | | - # Output: |
44 | | - # 📂 experiment |
45 | | - # ├── 📄 data |
46 | | - # └── 📄 config |
47 | 73 | ``` |
48 | 74 | """ |
49 | | - icon_map = { |
50 | | - False: "📄", |
51 | | - True: "📂", |
52 | | - None: "❓", |
53 | | - } |
| 75 | + node_icon = _get_node_icon(node, show_missing_indicator) |
| 76 | + line_prefix = _build_line_prefix(parents, is_last) |
| 77 | + node_label = _build_node_label(node, show_type, show_params) |
54 | 78 |
|
55 | | - node_icon = icon_map[node.is_collection] |
56 | | - if not node.has_data and show_missing_indicator: |
57 | | - node_icon += f"{icon_map[None]}" |
| 79 | + tree_representation = f"{line_prefix}{node_icon} {node_label}\n" |
58 | 80 |
|
59 | | - line_prefix = "" |
60 | | - for parent_is_last in parents[:-1]: |
61 | | - line_prefix += " " if parent_is_last else "│ " |
| 81 | + if node.is_collection and node.has_data: |
| 82 | + for i, child in enumerate(node.data): |
| 83 | + child_is_last = i == len(node.data) - 1 |
| 84 | + tree_representation += print_data_stream_tree( |
| 85 | + child, |
| 86 | + prefix="", |
| 87 | + is_last=child_is_last, |
| 88 | + parents=parents + [is_last], |
| 89 | + show_params=show_params, |
| 90 | + show_type=show_type, |
| 91 | + show_missing_indicator=show_missing_indicator, |
| 92 | + ) |
62 | 93 |
|
63 | | - if parents: |
64 | | - branch = "└── " if is_last else "├── " |
65 | | - line_prefix += branch |
| 94 | + return tree_representation |
66 | 95 |
|
67 | | - # Build node label with name, type, and parameters |
68 | | - node_label = node.name |
69 | 96 |
|
70 | | - if show_type: |
71 | | - node_label += f" [{node.__class__.__name__}]" |
| 97 | +def _get_html_header() -> str: |
| 98 | + """Returns the HTML header with CSS styles for the tree visualization.""" |
| 99 | + return """<!DOCTYPE html> |
| 100 | +<html lang="en"> |
| 101 | +<head> |
| 102 | + <meta charset="UTF-8"> |
| 103 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 104 | + <title>Data Stream Tree</title> |
| 105 | + <style> |
| 106 | + body { |
| 107 | + font-family: 'Courier New', monospace; |
| 108 | + padding: 20px; |
| 109 | + background-color: #f5f5f5; |
| 110 | + } |
| 111 | + .tree { |
| 112 | + background-color: white; |
| 113 | + padding: 20px; |
| 114 | + border-radius: 8px; |
| 115 | + box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| 116 | + white-space: pre; |
| 117 | + } |
| 118 | + .tree-node { |
| 119 | + position: relative; |
| 120 | + display: inline; |
| 121 | + } |
| 122 | + .tooltip { |
| 123 | + position: relative; |
| 124 | + cursor: help; |
| 125 | + } |
| 126 | + .tooltip .tooltiptext { |
| 127 | + visibility: hidden; |
| 128 | + background-color: #333; |
| 129 | + color: #fff; |
| 130 | + text-align: left; |
| 131 | + border-radius: 6px; |
| 132 | + padding: 8px 12px; |
| 133 | + position: absolute; |
| 134 | + z-index: 1; |
| 135 | + bottom: 125%; |
| 136 | + left: 50%; |
| 137 | + transform: translateX(-50%); |
| 138 | + white-space: normal; |
| 139 | + width: 300px; |
| 140 | + opacity: 0; |
| 141 | + transition: opacity 0.3s; |
| 142 | + font-family: Arial, sans-serif; |
| 143 | + font-size: 14px; |
| 144 | + box-shadow: 0 2px 8px rgba(0,0,0,0.2); |
| 145 | + } |
| 146 | + .tooltip .tooltiptext::after { |
| 147 | + content: ""; |
| 148 | + position: absolute; |
| 149 | + top: 100%; |
| 150 | + left: 50%; |
| 151 | + margin-left: -5px; |
| 152 | + border-width: 5px; |
| 153 | + border-style: solid; |
| 154 | + border-color: #333 transparent transparent transparent; |
| 155 | + } |
| 156 | + .tooltip:hover .tooltiptext { |
| 157 | + visibility: visible; |
| 158 | + opacity: 1; |
| 159 | + } |
| 160 | + .no-description { |
| 161 | + font-style: italic; |
| 162 | + color: #999; |
| 163 | + } |
| 164 | + </style> |
| 165 | +</head> |
| 166 | +<body> |
| 167 | + <div class="tree"> |
| 168 | +""" |
72 | 169 |
|
73 | | - if show_params and hasattr(node, "reader_params") and node.reader_params: |
74 | | - params_str = str(node.reader_params) |
75 | | - node_label += f" ({params_str})" |
76 | 170 |
|
77 | | - tree_representation = f"{line_prefix}{node_icon} {node_label}\n" |
| 171 | +def _get_tooltip_text(node: DataStream) -> str: |
| 172 | + import html as html_module |
| 173 | + |
| 174 | + parts = [] |
| 175 | + typ = getattr(node, "__class__", None) |
| 176 | + if typ: |
| 177 | + parts.append(f"Type: {html_module.escape(typ.__name__)}") |
| 178 | + else: |
| 179 | + parts.append("Type: Unknown") |
| 180 | + |
| 181 | + description = getattr(node, "description", None) |
| 182 | + if description: |
| 183 | + escaped_desc = html_module.escape(str(description)) |
| 184 | + parts.append(f"Description: {escaped_desc}") |
| 185 | + else: |
| 186 | + parts.append("Description: <em>No description available</em>") |
| 187 | + |
| 188 | + return "<br>".join(parts) |
| 189 | + |
| 190 | + |
| 191 | +def print_data_stream_tree_html( |
| 192 | + node: DataStream, |
| 193 | + is_last: bool = True, |
| 194 | + parents: list[bool] | None = None, |
| 195 | + show_params: bool = False, |
| 196 | + show_type: bool = False, |
| 197 | + show_missing_indicator: bool = True, |
| 198 | +) -> str: |
| 199 | + """Generates an HTML tree representation of a data stream hierarchy with tooltips. |
| 200 | +
|
| 201 | + Creates a formatted HTML string displaying the hierarchical structure of a data stream |
| 202 | + and its children as a tree with branch indicators, icons, and tooltips showing descriptions. |
| 203 | +
|
| 204 | + Args: |
| 205 | + node: The data stream node to start printing from. |
| 206 | + is_last: Whether this node is the last child of its parent. |
| 207 | + parents: List tracking whether each ancestor was a last child, used for drawing branches. |
| 208 | + show_params: Whether to render parameters of the datastream. |
| 209 | + show_type: Whether to render the class name of the datastream. |
| 210 | + show_missing_indicator: Whether to render the missing data indicator. |
| 211 | +
|
| 212 | + Returns: |
| 213 | + str: A formatted HTML string representing the data stream tree with tooltips. |
| 214 | +
|
| 215 | + Examples: |
| 216 | + ```python |
| 217 | + from contraqctor.contract import Dataset, csv, json |
| 218 | + from contraqctor.contract.utils import print_data_stream_tree_html |
| 219 | +
|
| 220 | + csv_stream = csv.Csv("data", reader_params=csv.CsvParams(path="data.csv")) |
| 221 | + json_stream = json.Json("config", reader_params=json.JsonParams(path="config.json")) |
| 222 | + dataset = Dataset("experiment", [csv_stream, json_stream], version="1.0.0") |
| 223 | +
|
| 224 | + html = print_data_stream_tree_html(dataset) |
| 225 | + with open("tree.html", "w") as f: |
| 226 | + f.write(html) |
| 227 | + ``` |
| 228 | + """ |
| 229 | + import html as html_module |
| 230 | + |
| 231 | + if parents is None: |
| 232 | + parents = [] |
| 233 | + |
| 234 | + html_header = _get_html_header() if not parents else "" |
| 235 | + node_icon = _get_node_icon(node, show_missing_indicator) |
| 236 | + line_prefix = _build_line_prefix(parents, is_last) |
| 237 | + node_label = _build_node_label(node, show_type, show_params) |
| 238 | + tooltip_text = _get_tooltip_text(node) |
| 239 | + |
| 240 | + node_label_escaped = html_module.escape(node_label) |
| 241 | + line_prefix_escaped = html_module.escape(line_prefix) |
| 242 | + |
| 243 | + tooltip_span = f'<span class="tooltiptext">{tooltip_text}</span>' |
| 244 | + node_content = f"{node_icon} {node_label_escaped}{tooltip_span}" |
| 245 | + tree_representation = f'{html_header}{line_prefix_escaped}<span class="tooltip">{node_content}</span>\n' |
78 | 246 |
|
79 | 247 | if node.is_collection and node.has_data: |
80 | 248 | for i, child in enumerate(node.data): |
81 | 249 | child_is_last = i == len(node.data) - 1 |
82 | | - tree_representation += print_data_stream_tree( |
| 250 | + tree_representation += print_data_stream_tree_html( |
83 | 251 | child, |
84 | | - prefix="", |
85 | 252 | is_last=child_is_last, |
86 | 253 | parents=parents + [is_last], |
87 | 254 | show_params=show_params, |
88 | 255 | show_type=show_type, |
89 | 256 | show_missing_indicator=show_missing_indicator, |
90 | 257 | ) |
91 | 258 |
|
| 259 | + if not parents: |
| 260 | + tree_representation += """ </div> |
| 261 | +</body> |
| 262 | +</html> |
| 263 | +""" |
| 264 | + |
92 | 265 | return tree_representation |
0 commit comments