Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow overriding area repr map section HTML #615

Merged
merged 4 commits into from
Aug 28, 2024
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
69 changes: 37 additions & 32 deletions pyresample/_formatting_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from __future__ import annotations

import uuid
from collections.abc import Iterable
from functools import lru_cache
from html import escape
from importlib.resources import read_binary
Expand Down Expand Up @@ -66,17 +67,26 @@


def plot_area_def(area: Union['geom.AreaDefinition', 'geom.SwathDefinition'], # noqa F821
fmt: Optional[Literal["svg", "png", None]] = None) -> Union[str, None]:
fmt: Optional[Literal["svg", "png", None]] = None,
features: Optional[Iterable[str]] = None,
) -> Union[str, None]:
"""Plot area.

Args:
area : Area/Swath to plot.
fmt : Output format of the plot. The output is the string representation of
the respective format xml for svg and base64 for png. Either svg or png.
If None (default) plot is just shown.
features: Series of string names of cartopy features to add to the plot.
Can be lowercase or uppercase names of the features, for example,
"land", "coastline", "borders", "ocean", or any other feature
available from ``cartopy.feature``. If None (default), then land,
coastline, and borders are used.

Returns:
svg or png image as string.
svg or png image as string or ``None`` when no format is provided
in which case the plot is shown interactively.

"""
import base64
from io import BytesIO, StringIO
Expand All @@ -98,11 +108,15 @@
ax.add_geometries([poly], crs=cartopy.crs.CRS(area.crs), facecolor="none", edgecolor="red")
bounds = poly.buffer(5).bounds
ax.set_extent([bounds[0], bounds[2], bounds[1], bounds[3]], crs=cartopy.crs.CRS(area.crs))
else:
raise NotImplementedError("Only AreaDefinition and SwathDefinition objects can be plotted")

Check warning on line 112 in pyresample/_formatting_html.py

View check run for this annotation

Codecov / codecov/patch

pyresample/_formatting_html.py#L112

Added line #L112 was not covered by tests

if features is None:
features = ("land", "coastline", "borders")

ax.add_feature(cartopy.feature.OCEAN)
ax.add_feature(cartopy.feature.LAND)
ax.add_feature(cartopy.feature.COASTLINE)
ax.add_feature(cartopy.feature.BORDERS)
for feat_name in features:
feat_obj = getattr(cartopy.feature, feat_name.upper())
ax.add_feature(feat_obj)

plt.tight_layout(pad=0)

Expand All @@ -111,14 +125,12 @@
plt.savefig(svg_str, format="svg", bbox_inches="tight")
plt.close()
return svg_str.getvalue()

elif fmt == "png":
png_str = BytesIO()
plt.savefig(png_str, format="png", bbox_inches="tight")
img_str = f"<img src='data:image/png;base64, {base64.encodebytes(png_str.getvalue()).decode('utf-8')}'/>"
plt.close()
return img_str

else:
plt.show()
return None
Expand Down Expand Up @@ -161,28 +173,6 @@
)


def map_section(area: Union['geom.AreaDefinition', 'geom.SwathDefinition']) -> str: # noqa F821
"""Create html for map section.

Args:
area : AreaDefinition or SwathDefinition.

Returns:
Html with collapsible section with a cartopy plot.

"""
map_icon = _icon("icon-globe")

if cartopy:
coll = collapsible_section("Map", details=plot_area_def(area, fmt="svg"), collapsed=True, icon=map_icon)
else:
coll = collapsible_section("Map",
details="Note: If cartopy is installed a display of the area can be seen here",
collapsed=True, icon=map_icon)

return f"{coll}"


def proj_area_attrs_section(area: 'geom.AreaDefinition') -> str: # noqa F821
"""Create html for attribute section based on an area Area.

Expand Down Expand Up @@ -308,7 +298,9 @@

def area_repr(area: Union['geom.AreaDefinition', 'geom.SwathDefinition'],
include_header: bool = True,
include_static_files: bool = True):
include_static_files: bool = True,
map_content: str | None = None,
):
"""Return html repr of an AreaDefinition.

Args:
Expand All @@ -318,6 +310,8 @@
display in the overview of area definitions for the Satpy documentation this
should be set to false.
include_static_files : Load and include css and html needed for representation.
map_content : Optionally override the map section contents. Can be any string
that is valid HTML between a "<div></div>" tag.

Returns:
Html.
Expand Down Expand Up @@ -347,7 +341,18 @@
html += "<div class='pyresample-area-sections'>"
if isinstance(area, geom.AreaDefinition):
html += proj_area_attrs_section(area)
html += map_section(area)
map_icon = _icon("icon-globe")
if map_content is None:
if cartopy:
map_content = plot_area_def(area, fmt="svg")
else:
map_content = "Note: If cartopy is installed a display of the area can be seen here"
coll = collapsible_section("Map",
details=map_content,
collapsed=True,
icon=map_icon)

html += str(coll)
elif isinstance(area, geom.SwathDefinition):
html += swath_area_attrs_section(area)

Expand Down
4 changes: 2 additions & 2 deletions pyresample/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2121,8 +2121,8 @@ def update_hash(self, existing_hash: Optional[_Hash] = None) -> _Hash:
if existing_hash is None:
existing_hash = hashlib.sha1() # nosec: B324
existing_hash.update(self.crs_wkt.encode('utf-8'))
existing_hash.update(np.array(self.shape)) # type: ignore[arg-type]
existing_hash.update(np.array(self.area_extent)) # type: ignore[arg-type]
existing_hash.update(np.array(self.shape))
existing_hash.update(np.array(self.area_extent))
return existing_hash

@daskify_2in_2out
Expand Down
38 changes: 22 additions & 16 deletions pyresample/test/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import unittest.mock as mock
from unittest.mock import ANY

import pytest

import pyresample
from pyresample._formatting_html import (
area_repr,
Expand All @@ -29,25 +31,21 @@
from .test_geometry.test_swath import _gen_swath_def_numpy, _gen_swath_def_xarray_dask


def test_plot_area_def_w_area_def(area_def_stere_source): # noqa F811
"""Test AreaDefinition plotting as svg/png."""
area = area_def_stere_source

with mock.patch('matplotlib.pyplot.savefig') as mock_savefig:
plot_area_def(area, fmt="svg")
mock_savefig.asser_called_with(ANY, format="svg", bbox_inches="tight")
mock_savefig.reset_mock()
plot_area_def(area, fmt="png")
mock_savefig.assert_called_with(ANY, format="png", bbox_inches="tight")


def test_plot_area_def_w_area_def_show(area_def_stere_source): # noqa F811
@pytest.mark.parametrize("format", ["svg", "png", None])
@pytest.mark.parametrize("features", [None, ("coastline",)])
def test_plot_area_def_w_area_def(area_def_stere_source, format, features): # noqa F811
"""Test AreaDefinition plotting as svg/png."""
area = area_def_stere_source

with mock.patch('matplotlib.pyplot.show') as mock_show_plot:
plot_area_def(area)
mock_show_plot.assert_called_once()
with mock.patch('matplotlib.pyplot.savefig') as mock_savefig, \
mock.patch('matplotlib.pyplot.show') as mock_show_plot:
plot_area_def(area, fmt=format)
if format is None:
mock_show_plot.assert_called_once()
mock_savefig.assert_not_called()
else:
mock_show_plot.assert_not_called()
mock_savefig.asser_called_with(ANY, format=format, bbox_inches="tight")


def test_plot_area_def_w_swath_def(create_test_swath):
Expand All @@ -74,6 +72,14 @@ def test_area_def_cartopy_installed(area_def_stere_source): # noqa F811
assert "Note: If cartopy is installed a display of the area can be seen here" not in area._repr_html_()


def test_area_repr_custom_map(area_def_stere_source): # noqa F811
"""Test custom map section of area repr."""
area = area_def_stere_source
res = area_repr(area, include_header=False, include_static_files=False,
map_content="TEST")
assert "TEST" in res


def test_area_repr_w_static_files(area_def_stere_source): # noqa F811
"""Test area representation with static files (css/icons) included."""
area_def = area_def_stere_source
Expand Down
Loading