Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Breaking changes
restore the prior defaults (:issue:`10657`).
By `Stephan Hoyer <https://github.com/shoyer>`_.

- The HTML reprs for :py:class:`DataArray`, :py:class:`Dataset` and
:py:class:`DataTree` have been tweaked to hide empty sections, consistent
with the text reprs. The ``DataTree`` HTML repr also now automatically expands
sub-groups (:pull:`10785`).
By `Stephan Hoyer <https://github.com/shoyer>`_.

Deprecations
~~~~~~~~~~~~

Expand Down
19 changes: 11 additions & 8 deletions xarray/core/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,7 +1092,7 @@ def inherited_vars(mapping: ChainMap) -> dict:
return {k: v for k, v in mapping.parents.items() if k not in mapping.maps[0]}


def _datatree_node_repr(node: DataTree, show_inherited: bool) -> str:
def _datatree_node_repr(node: DataTree, root: bool) -> str:
summary = [f"Group: {node.path}"]

col_width = _calculate_col_width(node.variables)
Expand All @@ -1103,11 +1103,11 @@ def _datatree_node_repr(node: DataTree, show_inherited: bool) -> str:
# Only show dimensions if also showing a variable or coordinates section.
show_dims = (
node._node_coord_variables
or (show_inherited and inherited_coords)
or (root and inherited_coords)
or node._data_variables
)

dim_sizes = node.sizes if show_inherited else node._node_dims
dim_sizes = node.sizes if root else node._node_dims

if show_dims:
# Includes inherited dimensions.
Expand All @@ -1121,7 +1121,7 @@ def _datatree_node_repr(node: DataTree, show_inherited: bool) -> str:
node_coords = node.to_dataset(inherit=False).coords
summary.append(coords_repr(node_coords, col_width=col_width, max_rows=max_rows))

if show_inherited and inherited_coords:
if root and inherited_coords:
summary.append(
inherited_coords_repr(node, col_width=col_width, max_rows=max_rows)
)
Expand All @@ -1139,7 +1139,7 @@ def _datatree_node_repr(node: DataTree, show_inherited: bool) -> str:
)

# TODO: only show indexes defined at this node, with a separate section for
# inherited indexes (if show_inherited=True)
# inherited indexes (if root=True)
display_default_indexes = _get_boolean_with_default(
"display_default_indexes", False
)
Expand All @@ -1165,16 +1165,19 @@ def datatree_repr(dt: DataTree) -> str:
header = f"<xarray.DataTree{name_info}>"

lines = [header]
show_inherited = True
root = True

for pre, fill, node in renderer:
if isinstance(node, str):
lines.append(f"{fill}{node}")
continue

node_repr = _datatree_node_repr(node, show_inherited=show_inherited)
show_inherited = False # only show inherited coords on the root
node_repr = _datatree_node_repr(node, root=root)
root = False # only the first node is the root

# TODO: figure out if we can restructure this logic to move child groups
# up higher in the repr, directly below the <xarray.DataTree> header.
# This would be more consistent with the HTML repr.
raw_repr_lines = node_repr.splitlines()

node_line = f"{pre}{raw_repr_lines[0]}"
Expand Down
256 changes: 136 additions & 120 deletions xarray/core/formatting_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import TYPE_CHECKING, Literal

from xarray.core.formatting import (
filter_nondefault_indexes,
inherited_vars,
inline_index_repr,
inline_variable_array_repr,
Expand Down Expand Up @@ -323,24 +324,33 @@ def array_repr(arr) -> str:
indexed_dims = {}

obj_type = f"xarray.{type(arr).__name__}"
arr_name = f"'{arr.name}'" if getattr(arr, "name", None) else ""
arr_name = escape(repr(arr.name)) if getattr(arr, "name", None) else ""

header_components = [
f"<div class='xr-obj-type'>{obj_type}</div>",
f"<div class='xr-array-name'>{arr_name}</div>",
f"<div class='xr-obj-name'>{arr_name}</div>",
format_dims(dims, indexed_dims),
]

sections = [array_section(arr)]

if hasattr(arr, "coords"):
sections.append(coord_section(arr.coords))
if arr.coords:
sections.append(coord_section(arr.coords))

if hasattr(arr, "xindexes"):
indexes = _get_indexes_dict(arr.xindexes)
sections.append(index_section(indexes))

sections.append(attr_section(arr.attrs))
display_default_indexes = _get_boolean_with_default(
"display_default_indexes", False
)
xindexes = filter_nondefault_indexes(
_get_indexes_dict(arr.xindexes), not display_default_indexes
)
if xindexes:
indexes = _get_indexes_dict(arr.xindexes)
sections.append(index_section(indexes))

if arr.attrs:
sections.append(attr_section(arr.attrs))

return _obj_repr(arr, header_components, sections)

Expand All @@ -350,28 +360,85 @@ def dataset_repr(ds) -> str:

header_components = [f"<div class='xr-obj-type'>{escape(obj_type)}</div>"]

sections = [
dim_section(ds),
coord_section(ds.coords),
datavar_section(ds.data_vars),
index_section(_get_indexes_dict(ds.xindexes)),
attr_section(ds.attrs),
]
sections = []

sections.append(dim_section(ds))

if ds.coords:
sections.append(coord_section(ds.coords))

sections.append(datavar_section(ds.data_vars))

display_default_indexes = _get_boolean_with_default(
"display_default_indexes", False
)
xindexes = filter_nondefault_indexes(
_get_indexes_dict(ds.xindexes), not display_default_indexes
)
if xindexes:
sections.append(index_section(xindexes))

if ds.attrs:
sections.append(attr_section(ds.attrs))

return _obj_repr(ds, header_components, sections)


def datatree_node_sections(node: DataTree, root: bool = False) -> list[str]:
from xarray.core.coordinates import Coordinates

ds = node._to_dataset_view(rebuild_dims=False, inherit=True)
node_coords = node.to_dataset(inherit=False).coords

# use this class to get access to .xindexes property
inherited_coords = Coordinates(
coords=inherited_vars(node._coord_variables),
indexes=inherited_vars(node._indexes),
)

# Only show dimensions if also showing a variable or coordinates section.
show_dims = (
node._node_coord_variables
or (root and inherited_coords)
or node._data_variables
)

sections = []

if node.children:
children_max_items = 1 if ds.data_vars else 6
sections.append(
children_section(node.children, max_items_collapse=children_max_items)
)

if show_dims:
sections.append(dim_section(ds))

if node_coords:
sections.append(coord_section(node_coords))

# only show inherited coordinates on the root
if root and inherited_coords:
sections.append(inherited_coord_section(inherited_coords))

if ds.data_vars:
sections.append(datavar_section(ds.data_vars))

if ds.attrs:
sections.append(attr_section(ds.attrs))

return sections


def summarize_datatree_children(children: Mapping[str, DataTree]) -> str:
MAX_CHILDREN = OPTIONS["display_max_children"]
n_children = len(children)

children_html = []
for i, (n, c) in enumerate(children.items()):
for i, child in enumerate(children.values()):
if i < ceil(MAX_CHILDREN / 2) or i >= ceil(n_children - MAX_CHILDREN / 2):
is_last = i == (n_children - 1)
children_html.append(
_wrap_datatree_repr(datatree_node_repr(n, c), end=is_last)
)
children_html.append(datatree_child_repr(child, end=is_last))
elif n_children > MAX_CHILDREN and i == ceil(MAX_CHILDREN / 2):
children_html.append("<div>...</div>")

Expand All @@ -388,7 +455,6 @@ def summarize_datatree_children(children: Mapping[str, DataTree]) -> str:
_mapping_section,
name="Groups",
details_func=summarize_datatree_children,
max_items_collapse=1,
max_option_name="display_max_children",
expand_option_name="display_expand_groups",
)
Expand All @@ -402,108 +468,58 @@ def summarize_datatree_children(children: Mapping[str, DataTree]) -> str:
)


def datatree_node_repr(group_title: str, node: DataTree, show_inherited=False) -> str:
from xarray.core.coordinates import Coordinates

header_components = [f"<div class='xr-obj-type'>{escape(group_title)}</div>"]

ds = node._to_dataset_view(rebuild_dims=False, inherit=True)
node_coords = node.to_dataset(inherit=False).coords

# use this class to get access to .xindexes property
inherited_coords = Coordinates(
coords=inherited_vars(node._coord_variables),
indexes=inherited_vars(node._indexes),
)

sections = [
children_section(node.children),
dim_section(ds),
coord_section(node_coords),
]

# only show inherited coordinates on the root
if show_inherited:
sections.append(inherited_coord_section(inherited_coords))

sections += [
datavar_section(ds.data_vars),
attr_section(ds.attrs),
]

return _obj_repr(ds, header_components, sections)


def _wrap_datatree_repr(r: str, end: bool = False) -> str:
def datatree_child_repr(node: DataTree, end: bool = False) -> str:
# Wrap DataTree HTML representation with a tee to the left of it.
#
# Enclosing HTML tag is a <div> with :code:`display: inline-grid` style.
#
# Turns:
# [ title ]
# | details |
# |_____________|
#
# into (A):
# |─ [ title ]
# | | details |
# | |_____________|
#
# or (B):
# └─ [ title ]
# | details |
# |_____________|
end = bool(end)
height = "100%" if end is False else "1.2em" # height of line

path = escape(node.path)
sections = datatree_node_sections(node, root=False)
section_items = "".join(f"<li class='xr-section-item'>{s}</li>" for s in sections)

# TODO: Can we make the group name clickable to toggle the sections below?
# This looks like it would require the input/label pattern used above.
html = f"""
<div class='xr-group-box'>
<div class='xr-group-box-vline' style='height: {height}'></div>
<div class='xr-group-box-hline'></div>
<div class='xr-group-box-contents'>
<div class='xr-header'>
<div class='xr-group-name'>{path}</div>
</div>
<ul class='xr-sections'>
{section_items}
</ul>
</div>
</div>
"""
Wrap HTML representation with a tee to the left of it.

Enclosing HTML tag is a <div> with :code:`display: inline-grid` style.
return "".join(t.strip() for t in html.split("\n"))

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(
[
"<div style='display: inline-grid; grid-template-columns: 0px 20px auto; width: 100%;'>",
"<div style='",
"grid-column-start: 1;",
"border-right: 0.2em solid;",
"border-color: var(--xr-border-color);",
f"height: {height};",
"width: 0px;",
"'>",
"</div>",
"<div style='",
"grid-column-start: 2;",
"grid-row-start: 1;",
"height: 1em;",
"width: 20px;",
"border-bottom: 0.2em solid;",
"border-color: var(--xr-border-color);",
"'>",
"</div>",
"<div style='",
"grid-column-start: 3;",
"'>",
r,
"</div>",
"</div>",
]
)

def datatree_repr(node: DataTree) -> str:
header_components = [
f"<div class='xr-obj-type'>xarray.{type(node).__name__}</div>",
]
if node.name is not None:
name = escape(repr(node.name))
header_components.append(f"<div class='xr-obj-name'>{name}</div>")

def datatree_repr(dt: DataTree) -> str:
obj_type = f"xarray.{type(dt).__name__}"
return datatree_node_repr(obj_type, dt, show_inherited=True)
sections = datatree_node_sections(node, root=True)
return _obj_repr(node, header_components, sections)
Loading
Loading