Skip to content
This repository was archived by the owner on Oct 24, 2024. It is now read-only.

Commit 28a79d1

Browse files
Enable tree-style HTML representation (#109)
* Modify HTML repr to add "anytree" style * Instead of indenting children, wrap each node_repr into a three column CSS grid * Column 1 has right border, column 2 has 2 rows, one with bottom border, column 3 contains the node_repr * Set the height of column 1 depending on whether it is the end of a list of nodes in the tree or not * Change _wrapped_node_repr -> _wrap_repr * This quick rewrite allows for much easier testing; the code is a bit more single purpose this way. * Unit tests for _wrap_repr and summarize_children. * black; quick flake8 patch (hashlib.sha1) was not used. * whatsnew Co-authored-by: Thomas Nicholas <[email protected]>
1 parent ab8eea9 commit 28a79d1

File tree

3 files changed

+287
-6
lines changed

3 files changed

+287
-6
lines changed

datatree/formatting_html.py

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,24 @@
1616

1717

1818
def summarize_children(children: Mapping[str, Any]) -> str:
19-
children_li = "".join(
20-
f"<ul class='xr-sections'>{node_repr(n, c)}</ul>" for n, c in children.items()
19+
N_CHILDREN = len(children) - 1
20+
21+
# Get result from node_repr and wrap it
22+
lines_callback = lambda n, c, end: _wrap_repr(node_repr(n, c), end=end)
23+
24+
children_html = "".join(
25+
lines_callback(n, c, end=False) # Long lines
26+
if i < N_CHILDREN
27+
else lines_callback(n, c, end=True) # Short lines
28+
for i, (n, c) in enumerate(children.items())
2129
)
2230

23-
return (
24-
"<ul class='xr-sections'>"
25-
f"<div style='padding-left:2rem;'>{children_li}<br></div>"
26-
"</ul>"
31+
return "".join(
32+
[
33+
"<div style='display: inline-grid; grid-template-columns: 100%'>",
34+
children_html,
35+
"</div>",
36+
]
2737
)
2838

2939

@@ -52,6 +62,77 @@ def node_repr(group_title: str, dt: Any) -> str:
5262
return _obj_repr(ds, header_components, sections)
5363

5464

65+
def _wrap_repr(r: str, end: bool = False) -> str:
66+
"""
67+
Wrap HTML representation with a tee to the left of it.
68+
69+
Enclosing HTML tag is a <div> with :code:`display: inline-grid` style.
70+
71+
Turns:
72+
[ title ]
73+
| details |
74+
|_____________|
75+
76+
into (A):
77+
|─ [ title ]
78+
| | details |
79+
| |_____________|
80+
81+
or (B):
82+
└─ [ title ]
83+
| details |
84+
|_____________|
85+
86+
Parameters
87+
----------
88+
r: str
89+
HTML representation to wrap.
90+
end: bool
91+
Specify if the line on the left should continue or end.
92+
93+
Default is True.
94+
95+
Returns
96+
-------
97+
str
98+
Wrapped HTML representation.
99+
100+
Tee color is set to the variable :code:`--xr-border-color`.
101+
"""
102+
# height of line
103+
end = bool(end)
104+
height = "100%" if end is False else "1.2em"
105+
return "".join(
106+
[
107+
"<div style='display: inline-grid;'>",
108+
"<div style='",
109+
"grid-column-start: 1;",
110+
"border-right: 0.2em solid;",
111+
"border-color: var(--xr-border-color);",
112+
f"height: {height};",
113+
"width: 0px;",
114+
"'>",
115+
"</div>",
116+
"<div style='",
117+
"grid-column-start: 2;",
118+
"grid-row-start: 1;",
119+
"height: 1em;",
120+
"width: 20px;",
121+
"border-bottom: 0.2em solid;",
122+
"border-color: var(--xr-border-color);",
123+
"'>",
124+
"</div>",
125+
"<div style='",
126+
"grid-column-start: 3;",
127+
"'>",
128+
"<ul class='xr-sections'>",
129+
r,
130+
"</ul>" "</div>",
131+
"</div>",
132+
]
133+
)
134+
135+
55136
def datatree_repr(dt: Any) -> str:
56137
obj_type = f"datatree.{type(dt).__name__}"
57138
return node_repr(obj_type, dt)
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import pytest
2+
import xarray as xr
3+
4+
from datatree import DataTree, formatting_html
5+
6+
7+
@pytest.fixture(scope="module", params=["some html", "some other html"])
8+
def repr(request):
9+
return request.param
10+
11+
12+
class Test_summarize_children:
13+
"""
14+
Unit tests for summarize_children.
15+
"""
16+
17+
func = staticmethod(formatting_html.summarize_children)
18+
19+
@pytest.fixture(scope="class")
20+
def childfree_tree_factory(self):
21+
"""
22+
Fixture for a child-free DataTree factory.
23+
"""
24+
from random import randint
25+
26+
def _childfree_tree_factory():
27+
return DataTree(
28+
data=xr.Dataset({"z": ("y", [randint(1, 100) for _ in range(3)])})
29+
)
30+
31+
return _childfree_tree_factory
32+
33+
@pytest.fixture(scope="class")
34+
def childfree_tree(self, childfree_tree_factory):
35+
"""
36+
Fixture for a child-free DataTree.
37+
"""
38+
return childfree_tree_factory()
39+
40+
@pytest.fixture(scope="function")
41+
def mock_node_repr(self, monkeypatch):
42+
"""
43+
Apply mocking for node_repr.
44+
"""
45+
46+
def mock(group_title, dt):
47+
"""
48+
Mock with a simple result
49+
"""
50+
return group_title + " " + str(id(dt))
51+
52+
monkeypatch.setattr(formatting_html, "node_repr", mock)
53+
54+
@pytest.fixture(scope="function")
55+
def mock_wrap_repr(self, monkeypatch):
56+
"""
57+
Apply mocking for _wrap_repr.
58+
"""
59+
60+
def mock(r, *, end, **kwargs):
61+
"""
62+
Mock by appending "end" or "not end".
63+
"""
64+
return r + " " + ("end" if end else "not end") + "//"
65+
66+
monkeypatch.setattr(formatting_html, "_wrap_repr", mock)
67+
68+
def test_empty_mapping(self):
69+
"""
70+
Test with an empty mapping of children.
71+
"""
72+
children = {}
73+
assert self.func(children) == (
74+
"<div style='display: inline-grid; grid-template-columns: 100%'>" "</div>"
75+
)
76+
77+
def test_one_child(self, childfree_tree, mock_wrap_repr, mock_node_repr):
78+
"""
79+
Test with one child.
80+
81+
Uses a mock of _wrap_repr and node_repr to essentially mock
82+
the inline lambda function "lines_callback".
83+
"""
84+
# Create mapping of children
85+
children = {"a": childfree_tree}
86+
87+
# Expect first line to be produced from the first child, and
88+
# wrapped as the last child
89+
first_line = f"a {id(children['a'])} end//"
90+
91+
assert self.func(children) == (
92+
"<div style='display: inline-grid; grid-template-columns: 100%'>"
93+
f"{first_line}"
94+
"</div>"
95+
)
96+
97+
def test_two_children(self, childfree_tree_factory, mock_wrap_repr, mock_node_repr):
98+
"""
99+
Test with two level deep children.
100+
101+
Uses a mock of _wrap_repr and node_repr to essentially mock
102+
the inline lambda function "lines_callback".
103+
"""
104+
105+
# Create mapping of children
106+
children = {"a": childfree_tree_factory(), "b": childfree_tree_factory()}
107+
108+
# Expect first line to be produced from the first child, and
109+
# wrapped as _not_ the last child
110+
first_line = f"a {id(children['a'])} not end//"
111+
112+
# Expect second line to be produced from the second child, and
113+
# wrapped as the last child
114+
second_line = f"b {id(children['b'])} end//"
115+
116+
assert self.func(children) == (
117+
"<div style='display: inline-grid; grid-template-columns: 100%'>"
118+
f"{first_line}"
119+
f"{second_line}"
120+
"</div>"
121+
)
122+
123+
124+
class Test__wrap_repr:
125+
"""
126+
Unit tests for _wrap_repr.
127+
"""
128+
129+
func = staticmethod(formatting_html._wrap_repr)
130+
131+
def test_end(self, repr):
132+
"""
133+
Test with end=True.
134+
"""
135+
r = self.func(repr, end=True)
136+
assert r == (
137+
"<div style='display: inline-grid;'>"
138+
"<div style='"
139+
"grid-column-start: 1;"
140+
"border-right: 0.2em solid;"
141+
"border-color: var(--xr-border-color);"
142+
"height: 1.2em;"
143+
"width: 0px;"
144+
"'>"
145+
"</div>"
146+
"<div style='"
147+
"grid-column-start: 2;"
148+
"grid-row-start: 1;"
149+
"height: 1em;"
150+
"width: 20px;"
151+
"border-bottom: 0.2em solid;"
152+
"border-color: var(--xr-border-color);"
153+
"'>"
154+
"</div>"
155+
"<div style='"
156+
"grid-column-start: 3;"
157+
"'>"
158+
"<ul class='xr-sections'>"
159+
f"{repr}"
160+
"</ul>"
161+
"</div>"
162+
"</div>"
163+
)
164+
165+
def test_not_end(self, repr):
166+
"""
167+
Test with end=False.
168+
"""
169+
r = self.func(repr, end=False)
170+
assert r == (
171+
"<div style='display: inline-grid;'>"
172+
"<div style='"
173+
"grid-column-start: 1;"
174+
"border-right: 0.2em solid;"
175+
"border-color: var(--xr-border-color);"
176+
"height: 100%;"
177+
"width: 0px;"
178+
"'>"
179+
"</div>"
180+
"<div style='"
181+
"grid-column-start: 2;"
182+
"grid-row-start: 1;"
183+
"height: 1em;"
184+
"width: 20px;"
185+
"border-bottom: 0.2em solid;"
186+
"border-color: var(--xr-border-color);"
187+
"'>"
188+
"</div>"
189+
"<div style='"
190+
"grid-column-start: 3;"
191+
"'>"
192+
"<ul class='xr-sections'>"
193+
f"{repr}"
194+
"</ul>"
195+
"</div>"
196+
"</div>"
197+
)

docs/source/whats-new.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ v0.0.7 (unreleased)
2323
New Features
2424
~~~~~~~~~~~~
2525

26+
- Improve the HTML repr by adding tree-style lines connecting groups and sub-groups (:pull:`109`).
27+
By `Benjamin Woods <https://github.com/benjaminwoods>`_.
28+
2629
Breaking changes
2730
~~~~~~~~~~~~~~~~
2831

0 commit comments

Comments
 (0)