Skip to content

Commit cb1fa81

Browse files
authored
Merge pull request #70 from AllenNeuralDynamics/html-pretty-print
Add HTML rendering for tree structure visualization
2 parents 405f588 + 574b34e commit cb1fa81

File tree

1 file changed

+203
-30
lines changed

1 file changed

+203
-30
lines changed

src/contraqctor/contract/utils.py

Lines changed: 203 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
11
from .base import DataStream
22

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+
335

436
def print_data_stream_tree(
537
node: DataStream,
@@ -32,61 +64,202 @@ def print_data_stream_tree(
3264
from contraqctor.contract import Dataset, csv, json
3365
from contraqctor.contract.utils import print_data_stream_tree
3466
35-
# Create a dataset with streams
3667
csv_stream = csv.Csv("data", reader_params=csv.CsvParams(path="data.csv"))
3768
json_stream = json.Json("config", reader_params=json.JsonParams(path="config.json"))
3869
dataset = Dataset("experiment", [csv_stream, json_stream], version="1.0.0")
3970
40-
# Print the tree
4171
tree = print_data_stream_tree(dataset)
4272
print(tree)
43-
# Output:
44-
# 📂 experiment
45-
# ├── 📄 data
46-
# └── 📄 config
4773
```
4874
"""
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)
5478

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"
5880

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+
)
6293

63-
if parents:
64-
branch = "└── " if is_last else "├── "
65-
line_prefix += branch
94+
return tree_representation
6695

67-
# Build node label with name, type, and parameters
68-
node_label = node.name
6996

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+
"""
72169

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})"
76170

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'
78246

79247
if node.is_collection and node.has_data:
80248
for i, child in enumerate(node.data):
81249
child_is_last = i == len(node.data) - 1
82-
tree_representation += print_data_stream_tree(
250+
tree_representation += print_data_stream_tree_html(
83251
child,
84-
prefix="",
85252
is_last=child_is_last,
86253
parents=parents + [is_last],
87254
show_params=show_params,
88255
show_type=show_type,
89256
show_missing_indicator=show_missing_indicator,
90257
)
91258

259+
if not parents:
260+
tree_representation += """ </div>
261+
</body>
262+
</html>
263+
"""
264+
92265
return tree_representation

0 commit comments

Comments
 (0)