Skip to content
93 changes: 74 additions & 19 deletions docs/user-guide/mpl.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"This guide covers:\n",
"1. Rasterizing Data onto a Cartopy {class}`~cartopy.mpl.geoaxes.GeoAxes`\n",
"2. Visualizing Data with {class}`~matplotlib.collections.PolyCollection`\n",
"3. Visualizing Grid Topology with {class}`~matplotlib.collections.LineCollection`"
"3. Visualizing Grid Topology"
]
},
{
Expand Down Expand Up @@ -353,7 +353,8 @@
"metadata": {},
"outputs": [],
"source": [
"poly_collection = uxds[\"bottomDepth\"].to_polycollection()"
"projection = ccrs.Robinson()\n",
"poly_collection = uxds[\"bottomDepth\"].to_polycollection(projection=projection)"
]
},
{
Expand All @@ -363,17 +364,14 @@
"metadata": {},
"outputs": [],
"source": [
"# disables grid lines\n",
"poly_collection.set_antialiased(False)\n",
"\n",
"poly_collection.set_cmap(\"Blues\")\n",
"\n",
"fig, ax = plt.subplots(\n",
" 1,\n",
" 1,\n",
" facecolor=\"w\",\n",
" constrained_layout=True,\n",
" subplot_kw=dict(projection=ccrs.Robinson()),\n",
" subplot_kw=dict(projection=projection),\n",
")\n",
"\n",
"ax.add_feature(cfeature.COASTLINE)\n",
Expand All @@ -389,6 +387,10 @@
"id": "22",
"metadata": {},
"source": [
"```{important}\n",
"By default, `periodic_elements` is set to `\"exclude\"`. \n",
"```\n",
"\n",
"To reduce the number of polygons in the collection, you can [subset](./subset) before converting."
]
},
Expand All @@ -401,15 +403,15 @@
"source": [
"lon_bounds = (-50, 50)\n",
"lat_bounds = (-20, 20)\n",
"b = 5 # buffer for the selection so we can fill the plot area\n",
"b = 15 # buffer for the selection so we can fill the plot area\n",
"\n",
"poly_collection = (\n",
" uxds[\"bottomDepth\"]\n",
" .subset.bounding_box(\n",
" lon_bounds=(lon_bounds[0] - b, lon_bounds[1] + b),\n",
" lat_bounds=(lat_bounds[0] - b, lat_bounds[1] + b),\n",
" )\n",
" .to_polycollection()\n",
" .to_polycollection(projection=projection)\n",
")\n",
"\n",
"poly_collection.set_cmap(\"Blues\")\n",
Expand All @@ -420,30 +422,81 @@
" figsize=(7, 3.5),\n",
" facecolor=\"w\",\n",
" constrained_layout=True,\n",
" subplot_kw=dict(projection=ccrs.Robinson()),\n",
" subplot_kw=dict(projection=projection),\n",
")\n",
"\n",
"ax.set_extent(lon_bounds + lat_bounds, crs=PlateCarree())\n",
"ax.set_extent(lon_bounds + lat_bounds)\n",
"\n",
"ax.add_feature(cfeature.COASTLINE)\n",
"ax.add_feature(cfeature.BORDERS)\n",
"\n",
"ax.add_collection(poly_collection)\n",
"plt.title(\"PolyCollection\")"
"plt.title(\"subset PolyCollection\")"
]
},
{
"cell_type": "markdown",
"id": "206c73f6",
"metadata": {},
"source": [
"```{tip}\n",
"When working with a fine grid, matplotlib's anti-alias algorithm can sometimes produce unexpected visualization effects that look like missing patches or a white \"hazing\" from the fine slivers between the polygons. Try using `poly_collection.set_edgecolor('face)` and adjusting with `poly_collection.set_linewidth()` if needed.\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "24",
"metadata": {},
"source": [
"### Visualize Grid Topology with `LineCollection`\n",
"### Visualize Grid Topology\n"
]
},
{
"cell_type": "markdown",
"id": "36ddaa74",
"metadata": {},
"source": [
"#### Using `PolyCollection`\n",
"To visualize the unstructured grid geometry, you can set the edge color on an existing `PolyCollection`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6e1970f9",
"metadata": {},
"outputs": [],
"source": [
"projection = ccrs.Robinson()\n",
"poly_collection = uxds[\"bottomDepth\"].to_polycollection(projection=projection)\n",
"\n",
"To visualize the unstructured grid geometry, you can convert a {class}`~uxarray.Grid` into a {class}`~matplotlib.collections.LineCollection`, which stores the edges of the unstructured grid.\n",
"poly_collection.set_cmap(\"Blues\")\n",
"poly_collection.set_edgecolor(\"black\")\n",
"\n",
"```{important}\n",
"Since the transform for the {class}`~matplotlib.collections.LineCollection` and {class}`~matplotlib.collections.PolyCollection` are set to `ccrs.Geodetic()`, the edges and polygons are drawn correctly on the surface of a sphere and properly at the antimeridian.\n",
"```"
"fig, ax = plt.subplots(\n",
" 1,\n",
" 1,\n",
" facecolor=\"w\",\n",
" constrained_layout=True,\n",
" subplot_kw=dict(projection=projection),\n",
")\n",
"\n",
"ax.add_feature(cfeature.COASTLINE)\n",
"ax.add_feature(cfeature.BORDERS)\n",
"\n",
"ax.add_collection(poly_collection)\n",
"ax.set_global()\n",
"plt.title(\"PolyCollection with set_edgecolor\")"
]
},
{
"cell_type": "markdown",
"id": "f45dbc72",
"metadata": {},
"source": [
"#### Using `LineCollection`\n",
"You can also convert a {class}`~uxarray.Grid` into a {class}`~matplotlib.collections.LineCollection`, which stores the edges of the unstructured grid."
]
},
{
Expand All @@ -453,7 +506,9 @@
"metadata": {},
"outputs": [],
"source": [
"line_collection = uxds.uxgrid.to_linecollection(colors=\"black\", linewidths=0.5)"
"line_collection = uxds.uxgrid.to_linecollection(\n",
" colors=\"black\", linewidths=0.5, periodic_elements=\"split\"\n",
")"
]
},
{
Expand Down Expand Up @@ -481,7 +536,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "uxarray-viz-testing",
"language": "python",
"name": "python3"
},
Expand All @@ -495,7 +550,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.5"
"version": "3.13.11"
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion test/core/test_dataarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def test_to_polycollection(gridpath, datasetpath):
uxds_geoflow['v1'].to_polycollection()

# grid conversion
pc_geoflow_grid = uxds_geoflow.uxgrid.to_polycollection()
pc_geoflow_grid = uxds_geoflow.uxgrid.to_polycollection(periodic_elements="ignore")

# number of elements
assert len(pc_geoflow_grid._paths) == uxds_geoflow.uxgrid.n_face
Expand Down
9 changes: 0 additions & 9 deletions test/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,3 @@ def test_to_raster_pixel_ratio(gridpath, r1, r2):
f = r2 / r1
d = np.array(raster2.shape) - f * np.array(raster1.shape)
assert (d >= 0).all() and (d <= f - 1).all()


def test_collections_projection_kwarg(gridpath):
import cartopy.crs as ccrs
uxgrid = ux.open_grid(gridpath("ugrid", "outCSne30", "outCSne30.ug"))

with pytest.warns(FutureWarning):
pc = uxgrid.to_polycollection(projection=ccrs.PlateCarree())
lc = uxgrid.to_linecollection(projection=ccrs.PlateCarree())
71 changes: 65 additions & 6 deletions uxarray/core/dataarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import warnings
from html import escape
from typing import TYPE_CHECKING, Any, Hashable, Literal, Mapping
from typing import TYPE_CHECKING, Any, Hashable, Literal, Mapping, Optional
from warnings import warn

import cartopy.crs as ccrs
import numpy as np
import xarray as xr
from cartopy.mpl.geoaxes import GeoAxes
Expand Down Expand Up @@ -275,6 +276,11 @@ def to_geodataframe(

def to_polycollection(
self,
periodic_elements: Optional[str] = "exclude",
projection: Optional[ccrs.Projection] = None,
return_indices: Optional[bool] = False,
cache: Optional[bool] = True,
override: Optional[bool] = False,
**kwargs,
):
"""Constructs a ``matplotlib.collections.PolyCollection``` consisting
Expand All @@ -283,8 +289,19 @@ def to_polycollection(

Parameters
----------
**kwargs: dict
Key word arguments to pass into the construction of a PolyCollection
periodic_elements : str, optional
Method for handling periodic elements. One of ['exclude', 'split', or 'ignore']:
- 'exclude': Periodic elements will be identified and excluded from the GeoDataFrame
- 'split': Periodic elements will be identified and split using the ``antimeridian`` package
- 'ignore': No processing will be applied to periodic elements.
projection: ccrs.Projection
Cartopy geographic projection to use
return_indices: bool
Flag to indicate whether to return the indices of corrected polygons, if any exist
cache: bool
Flag to indicate whether to cache the computed PolyCollection
override: bool
Flag to indicate whether to override a cached PolyCollection, if it exists
"""
# data is multidimensional, must be a 1D slice
if self.values.ndim > 1:
Expand All @@ -293,11 +310,53 @@ def to_polycollection(
f"for face-centered data."
)

poly_collection = self.uxgrid.to_polycollection(**kwargs)
if self._face_centered():
poly_collection, corrected_to_original_faces = (
self.uxgrid.to_polycollection(
override=override,
cache=cache,
periodic_elements=periodic_elements,
return_indices=True,
projection=projection,
**kwargs,
)
)

poly_collection.set_array(self.data)
if periodic_elements == "exclude":
# index data to ignore data mapped to periodic elements
_data = np.delete(
self.values,
self.uxgrid._poly_collection_cached_parameters[
"antimeridian_face_indices"
],
axis=0,
)
elif periodic_elements == "split":
_data = self.values[corrected_to_original_faces]
else:
_data = self.values

return poly_collection
if (
self.uxgrid._poly_collection_cached_parameters[
"non_nan_polygon_indices"
]
is not None
):
# index data to ignore NaN polygons
_data = _data[
self.uxgrid._poly_collection_cached_parameters[
"non_nan_polygon_indices"
]
]

poly_collection.set_array(_data)

if return_indices:
return poly_collection, corrected_to_original_faces
else:
return poly_collection
else:
raise ValueError("Data variable must be face centered.")

def to_raster(
self,
Expand Down
18 changes: 8 additions & 10 deletions uxarray/grid/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ def _grid_to_matplotlib_polycollection(
# Handle unsupported configuration: splitting periodic elements with projection
if periodic_elements == "split" and projection is not None:
raise ValueError(
"Explicitly projecting lines is not supported. Please pass in your projection"
"Explicitly projecting lines is not supported. Please pass in your projection "
"using the 'transform' parameter"
)

Expand All @@ -475,15 +475,6 @@ def _grid_to_matplotlib_polycollection(
central_longitude=central_longitude,
)

# Filter out degenerate polygons, which cause issues with Cartopy 0.25
# fmt: off
degenerate = (
(polygon_shells[:,0,0][:,np.newaxis] == polygon_shells[:,1:,0]).all(axis=1)
| (polygon_shells[:,0,1][:,np.newaxis] == polygon_shells[:,1:,1]).all(axis=1)
)
# fmt: on
polygon_shells = polygon_shells[~degenerate, ...]

# Projected polygon shells if a projection is specified
if projection is not None:
projected_polygon_shells = _build_polygon_shells(
Expand Down Expand Up @@ -516,6 +507,13 @@ def _grid_to_matplotlib_polycollection(
does_not_contain_nan = ~np.isnan(shells_d).any(axis=(1, 2))
non_nan_polygon_indices = np.where(does_not_contain_nan)[0]

grid._poly_collection_cached_parameters["non_nan_polygon_indices"] = (
non_nan_polygon_indices
)
grid._poly_collection_cached_parameters["antimeridian_face_indices"] = (
antimeridian_face_indices
)

# Select which shells to use: projected or original
if projected_polygon_shells is not None:
shells_to_use = projected_polygon_shells
Expand Down
Loading
Loading