diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py
index 2c76b182207..384b6f09a2a 100644
--- a/xarray/core/formatting_html.py
+++ b/xarray/core/formatting_html.py
@@ -2,9 +2,11 @@
import uuid
from collections import OrderedDict
+from collections.abc import Mapping
from functools import lru_cache, partial
from html import escape
from importlib.resources import files
+from typing import Any
from xarray.core.formatting import (
inline_index_repr,
@@ -18,6 +20,10 @@
("xarray.static.css", "style.css"),
)
+from xarray.core.options import OPTIONS
+
+OPTIONS["display_expand_groups"] = "default"
+
@lru_cache(None)
def _load_static_files():
@@ -341,3 +347,128 @@ def dataset_repr(ds) -> str:
]
return _obj_repr(ds, header_components, sections)
+
+
+def summarize_children(children: Mapping[str, Any]) -> str:
+ N_CHILDREN = len(children) - 1
+
+ # Get result from node_repr and wrap it
+ lines_callback = lambda n, c, end: _wrap_repr(node_repr(n, c), end=end)
+
+ children_html = "".join(
+ (
+ lines_callback(n, c, end=False) # Long lines
+ if i < N_CHILDREN
+ else lines_callback(n, c, end=True)
+ ) # Short lines
+ for i, (n, c) in enumerate(children.items())
+ )
+
+ return "".join(
+ [
+ "
with :code:`display: inline-grid` style.
+
+ Turns:
+ [ title ]
+ | details |
+ |_____________|
+
+ into (A):
+ |─ [ title ]
+ | | details |
+ | |_____________|
+
+ or (B):
+ └─ [ title ]
+ | details |
+ |_____________|
+
+ Parameters
+ ----------
+ r: str
+ HTML representation to wrap.
+ end: bool
+ Specify if the line on the left should continue or end.
+
+ Default is True.
+
+ Returns
+ -------
+ str
+ Wrapped HTML representation.
+
+ Tee color is set to the variable :code:`--xr-border-color`.
+ """
+ # height of line
+ end = bool(end)
+ height = "100%" if end is False else "1.2em"
+ return "".join(
+ [
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ ]
+ )
+
+
+def datatree_repr(dt: Any) -> str:
+ obj_type = f"datatree.{type(dt).__name__}"
+ return node_repr(obj_type, dt)
diff --git a/xarray/tests/test_formatting_html.py b/xarray/tests/test_formatting_html.py
index 6540406e914..61abaa24b74 100644
--- a/xarray/tests/test_formatting_html.py
+++ b/xarray/tests/test_formatting_html.py
@@ -7,6 +7,7 @@
import xarray as xr
from xarray.core import formatting_html as fh
from xarray.core.coordinates import Coordinates
+from xarray.core.datatree import DataTree
@pytest.fixture
@@ -196,3 +197,196 @@ def test_nonstr_variable_repr_html() -> None:
html = v._repr_html_().strip()
assert "
22 :bar" in html
assert "
10: 3" in html
+
+
+@pytest.fixture(scope="module", params=["some html", "some other html"])
+def repr(request):
+ return request.param
+
+
+class Test_summarize_children:
+ """
+ Unit tests for summarize_children.
+ """
+
+ func = staticmethod(fh.summarize_children)
+
+ @pytest.fixture(scope="class")
+ def childfree_tree_factory(self):
+ """
+ Fixture for a child-free DataTree factory.
+ """
+ from random import randint
+
+ def _childfree_tree_factory():
+ return DataTree(
+ data=xr.Dataset({"z": ("y", [randint(1, 100) for _ in range(3)])})
+ )
+
+ return _childfree_tree_factory
+
+ @pytest.fixture(scope="class")
+ def childfree_tree(self, childfree_tree_factory):
+ """
+ Fixture for a child-free DataTree.
+ """
+ return childfree_tree_factory()
+
+ @pytest.fixture(scope="function")
+ def mock_node_repr(self, monkeypatch):
+ """
+ Apply mocking for node_repr.
+ """
+
+ def mock(group_title, dt):
+ """
+ Mock with a simple result
+ """
+ return group_title + " " + str(id(dt))
+
+ monkeypatch.setattr(fh, "node_repr", mock)
+
+ @pytest.fixture(scope="function")
+ def mock_wrap_repr(self, monkeypatch):
+ """
+ Apply mocking for _wrap_repr.
+ """
+
+ def mock(r, *, end, **kwargs):
+ """
+ Mock by appending "end" or "not end".
+ """
+ return r + " " + ("end" if end else "not end") + "//"
+
+ monkeypatch.setattr(fh, "_wrap_repr", mock)
+
+ def test_empty_mapping(self):
+ """
+ Test with an empty mapping of children.
+ """
+ children = {}
+ assert self.func(children) == (
+ "
" "
"
+ )
+
+ def test_one_child(self, childfree_tree, mock_wrap_repr, mock_node_repr):
+ """
+ Test with one child.
+
+ Uses a mock of _wrap_repr and node_repr to essentially mock
+ the inline lambda function "lines_callback".
+ """
+ # Create mapping of children
+ children = {"a": childfree_tree}
+
+ # Expect first line to be produced from the first child, and
+ # wrapped as the last child
+ first_line = f"a {id(children['a'])} end//"
+
+ assert self.func(children) == (
+ "
"
+ f"{first_line}"
+ "
"
+ )
+
+ def test_two_children(self, childfree_tree_factory, mock_wrap_repr, mock_node_repr):
+ """
+ Test with two level deep children.
+
+ Uses a mock of _wrap_repr and node_repr to essentially mock
+ the inline lambda function "lines_callback".
+ """
+
+ # Create mapping of children
+ children = {"a": childfree_tree_factory(), "b": childfree_tree_factory()}
+
+ # Expect first line to be produced from the first child, and
+ # wrapped as _not_ the last child
+ first_line = f"a {id(children['a'])} not end//"
+
+ # Expect second line to be produced from the second child, and
+ # wrapped as the last child
+ second_line = f"b {id(children['b'])} end//"
+
+ assert self.func(children) == (
+ "
"
+ f"{first_line}"
+ f"{second_line}"
+ "
"
+ )
+
+
+class Test__wrap_repr:
+ """
+ Unit tests for _wrap_repr.
+ """
+
+ func = staticmethod(fh._wrap_repr)
+
+ def test_end(self, repr):
+ """
+ Test with end=True.
+ """
+ r = self.func(repr, end=True)
+ assert r == (
+ "
"
+ "
"
+ "
"
+ "
"
+ "
"
+ "
"
+ "
"
+ )
+
+ def test_not_end(self, repr):
+ """
+ Test with end=False.
+ """
+ r = self.func(repr, end=False)
+ assert r == (
+ "
"
+ "
"
+ "
"
+ "
"
+ "
"
+ "
"
+ "
"
+ )