Skip to content

Commit

Permalink
HTML Repr for Grid, UxDataArray and UxDataset (#868)
Browse files Browse the repository at this point in the history
* start formatting for repr in html

* add html repr for grid, uxdataset, uxdataarray

* add html repr for grid, uxdataset, uxdataarray

* add comments

* re-add deleted pyproject.toml

* generalize header name for grid that is part of another obj

* rename xarray to uxarray in array header

* update grid repr

* add tests for repr
  • Loading branch information
philipc2 authored Jul 31, 2024
1 parent 05bd3a0 commit f388e68
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 2 deletions.
37 changes: 37 additions & 0 deletions test/test_repr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import uxarray as ux
import os

import pytest

from pathlib import Path

current_path = Path(os.path.dirname(os.path.realpath(__file__)))


grid_path = current_path / 'meshfiles' / "ugrid" / "quad-hexagon" / 'grid.nc'
data_path = current_path / 'meshfiles' / "ugrid" / "quad-hexagon" / 'data.nc'



def test_grid_repr():
uxgrid = ux.open_grid(grid_path)

out = uxgrid._repr_html_()

assert out is not None


def test_dataset_repr():
uxds = ux.open_dataset(grid_path, data_path)

out = uxds._repr_html_()

assert out is not None


def test_dataarray_repr():
uxds = ux.open_dataset(grid_path, data_path)

out = uxds['t2m']._repr_html_()

assert out is not None
13 changes: 11 additions & 2 deletions uxarray/conventions/descriptors.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
DESCRIPTOR_NAMES = ["face_areas", "edge_face_distances", "edge_node_distances"]
DESCRIPTOR_NAMES = [
"face_areas",
"n_nodes_per_face",
"edge_face_distances",
"edge_node_distances",
]


FACE_AREAS_DIMS = ["n_face"]

FACE_AREAS_ATTRS = {"cf_role": "face_areas"}
FACE_AREAS_ATTRS = {"cf_role": "face_areas", "long_name": "Area of each face."}


# TODO: add n_nodes_per_face


EDGE_FACE_DISTANCES_DIMS = ["n_edge"]
EDGE_FACE_DISTANCES_ATTRS = {
Expand Down
12 changes: 12 additions & 0 deletions uxarray/core/dataarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
import xarray as xr
import numpy as np


from typing import TYPE_CHECKING, Optional, Union, Hashable, Literal

from uxarray.formatting_html import array_repr

from html import escape

from xarray.core.options import OPTIONS

from uxarray.grid import Grid
import uxarray.core.dataset

Expand Down Expand Up @@ -78,6 +85,11 @@ def __init__(self, *args, uxgrid: Grid = None, **kwargs):
subset = UncachedAccessor(DataArraySubsetAccessor)
remap = UncachedAccessor(UxDataArrayRemapAccessor)

def _repr_html_(self) -> str:
if OPTIONS["display_style"] == "text":
return f"<pre>{escape(repr(self))}</pre>"
return array_repr(self)

@classmethod
def _construct_direct(cls, *args, **kwargs):
"""Override to make the result a ``uxarray.UxDataArray`` class."""
Expand Down
11 changes: 11 additions & 0 deletions uxarray/core/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

from xarray.core.utils import UncachedAccessor

from uxarray.formatting_html import dataset_repr

from html import escape

from xarray.core.options import OPTIONS

from uxarray.remap import UxDatasetRemapAccessor

from warnings import warn
Expand Down Expand Up @@ -76,6 +82,11 @@ def __init__(
plot = UncachedAccessor(UxDatasetPlotAccessor)
remap = UncachedAccessor(UxDatasetRemapAccessor)

def _repr_html_(self) -> str:
if OPTIONS["display_style"] == "text":
return f"<pre>{escape(repr(self))}</pre>"
return dataset_repr(self)

def __getitem__(self, key):
"""Override to make sure the result is an instance of
``uxarray.UxDataArray`` or ``uxarray.UxDataset``."""
Expand Down
1 change: 1 addition & 0 deletions uxarray/formatting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pass
205 changes: 205 additions & 0 deletions uxarray/formatting_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
from html import escape
import xarray.core.formatting_html as xrfm

from functools import partial

from uxarray.conventions import ugrid, descriptors

from collections import OrderedDict


def _grid_header(grid, header_name=None):
if header_name is None:
obj_type = f"uxarray.{type(grid).__name__}"
else:
obj_type = f"{header_name}"

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

return header_components


def _grid_sections(grid, max_items_collapse=15):
cartesian_coordinates = list(
[coord for coord in ugrid.CARTESIAN_COORDS if coord in grid._ds]
)
spherical_coordinates = list(
[coord for coord in ugrid.SPHERICAL_COORDS if coord in grid._ds]
)
descritor = list(
[desc for desc in descriptors.DESCRIPTOR_NAMES if desc in grid._ds]
)
connectivity = grid.connectivity

sections = [xrfm.dim_section(grid._ds)]

sections.append(
grid_spherical_coordinates_section(
grid._ds[spherical_coordinates],
max_items_collapse=max_items_collapse,
name="Spherical Coordinates",
)
)
sections.append(
grid_cartesian_coordinates_section(
grid._ds[cartesian_coordinates],
max_items_collapse=max_items_collapse,
name="Cartesian Coordinates",
)
)

sections.append(
grid_connectivity_section(
grid._ds[connectivity],
max_items_collapse=max_items_collapse,
name="Connectivity",
)
)

sections.append(
grid_descriptor_section(
grid._ds[descritor],
max_items_collapse=max_items_collapse,
name="Descriptors",
)
)

sections.append(
grid_attr_section(
grid._ds.attrs, max_items_collapse=max_items_collapse, name="Attributes"
)
)

return sections


def grid_repr(grid, max_items_collapse=15, header_name=None) -> str:
"""HTML repr for ``Grid`` class."""
header_components = _grid_header(grid, header_name)

sections = _grid_sections(grid, max_items_collapse)

return xrfm._obj_repr(grid, header_components, sections)


grid_spherical_coordinates_section = partial(
xrfm._mapping_section,
details_func=xrfm.summarize_vars,
expand_option_name="display_expand_data_vars",
)

grid_cartesian_coordinates_section = partial(
xrfm._mapping_section,
details_func=xrfm.summarize_vars,
expand_option_name="display_expand_data_vars",
)

grid_connectivity_section = partial(
xrfm._mapping_section,
details_func=xrfm.summarize_vars,
expand_option_name="display_expand_data_vars",
)

grid_descriptor_section = partial(
xrfm._mapping_section,
details_func=xrfm.summarize_vars,
expand_option_name="display_expand_data_vars",
)

grid_attr_section = partial(
xrfm._mapping_section,
details_func=xrfm.summarize_attrs,
expand_option_name="display_expand_attrs",
)


def _obj_repr_with_grid(obj, header_components, sections):
"""Return HTML repr of an uxarray object.
If CSS is not injected (untrusted notebook), fallback to the plain
text repr.
"""
# Construct header and sections for the main object
header = f"<div class='xr-header'>{''.join(h for h in header_components)}</div>"
sections = "".join(f"<li class='xr-section-item'>{s}</li>" for s in sections)

grid_html_repr = grid_repr(
obj.uxgrid,
max_items_collapse=0,
header_name=f"uxarray.{type(obj).__name__}.uxgrid",
)

icons_svg, css_style = xrfm._load_static_files()
obj_repr_html = (
"<div>"
f"{icons_svg}<style>{css_style}</style>"
f"<pre class='xr-text-repr-fallback'>{escape(repr(obj))}</pre>"
"<div class='xr-wrap' style='display:none'>"
f"{header}"
f"<ul class='xr-sections'>{sections}</ul>"
"</div>"
"</div>"
)

return (
"<div>"
f"{icons_svg}<style>{css_style}</style>"
f"<pre class='xr-text-repr-fallback'>{escape(repr(obj))}</pre>"
"<div class='xr-wrap' style='display:none'>"
f"{obj_repr_html}"
"<details>"
"<summary>Show Grid Information</summary>"
f"{grid_html_repr}"
"</details>"
"</div>"
"</div>"
)


def dataset_repr(ds) -> str:
"""HTML repr for ``UxDataset`` class."""
obj_type = f"uxarray.{type(ds).__name__}"

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

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

return _obj_repr_with_grid(ds, header_components, sections)


def array_repr(arr) -> str:
"""HTML repr for ``UxDataArray`` class."""

dims = OrderedDict((k, v) for k, v in zip(arr.dims, arr.shape))
if hasattr(arr, "xindexes"):
indexed_dims = arr.xindexes.dims
else:
indexed_dims = {}

obj_type = f"uxarray.{type(arr).__name__}"
arr_name = f"'{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>",
xrfm.format_dims(dims, indexed_dims),
]

sections = [xrfm.array_section(arr)]

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

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

sections.append(xrfm.attr_section(arr.attrs))

return _obj_repr_with_grid(arr, header_components, sections)
11 changes: 11 additions & 0 deletions uxarray/grid/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import xarray as xr
import numpy as np

from html import escape

from xarray.core.options import OPTIONS

from typing import (
Optional,
Union,
Expand All @@ -23,6 +27,8 @@
from uxarray.io._topology import _read_topology
from uxarray.io._geos import _read_geos_cs

from uxarray.formatting_html import grid_repr

from uxarray.io.utils import _parse_grid_type
from uxarray.grid.area import get_all_face_area_from_coords
from uxarray.grid.coordinates import (
Expand Down Expand Up @@ -396,6 +402,11 @@ def __repr__(self):
+ descriptors_str
)

def _repr_html_(self) -> str:
if OPTIONS["display_style"] == "text":
return f"<pre>{escape(repr(self))}</pre>"
return grid_repr(self)

def __getitem__(self, item):
"""Implementation of getitem operator for indexing a grid to obtain
variables.
Expand Down

0 comments on commit f388e68

Please sign in to comment.