diff --git a/docs/api.rst b/docs/api.rst index df323f2f8..618a52919 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -241,6 +241,7 @@ Grid Grid.to_geodataframe Grid.to_polycollection Grid.to_linecollection + Grid.plot_edges Grid.to_xarray UxDataArray diff --git a/docs/user-guide/mpl.ipynb b/docs/user-guide/mpl.ipynb index c61d436a3..520f1d38e 100644 --- a/docs/user-guide/mpl.ipynb +++ b/docs/user-guide/mpl.ipynb @@ -477,6 +477,38 @@ "ax.set_title(\"LineCollection\")\n", "plt.show()" ] + }, + { + "cell_type": "markdown", + "id": "1f1e0022-ad9e-44f3-b267-b43030d69e88", + "metadata": {}, + "source": [ + "To plot only the visible edges of a grid within a local extent, use the {meth}`~uxarray.Grid.plot_edges` method.\n", + "This routine automatically extracts the current axis limits and draws only the edges that fall inside those limits.\n", + "By avoiding the rendering of off‑screen edges, it can dramatically reduce the amount of work required for high‑resolution grids, leading to faster plot generation and lower memory consumption." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e544b3ab-a045-440b-b9bf-b7f11b5d65bf", + "metadata": {}, + "outputs": [], + "source": [ + "center = -50, 0\n", + "fig, ax = plt.subplots(\n", + " 1,\n", + " 1,\n", + " constrained_layout=True,\n", + " subplot_kw={\"projection\": ccrs.Orthographic(*center)},\n", + ")\n", + "ax.add_feature(cfeature.LAND)\n", + "ax.add_feature(cfeature.COASTLINE)\n", + "ax.set_extent([center[0] + 20, center[0] - 20, center[1] - 10, center[1] + 10])\n", + "\n", + "uxds.uxgrid.plot_edges(ax=ax, color=\"blue\")\n", + "plt.show()" + ] } ], "metadata": { diff --git a/test/test_plot.py b/test/test_plot.py index 8af794c90..4f9312df1 100644 --- a/test/test_plot.py +++ b/test/test_plot.py @@ -4,6 +4,7 @@ import pytest import numpy as np +import matplotlib import matplotlib.pyplot as plt import cartopy.crs as ccrs @@ -217,3 +218,13 @@ def test_collections_projection_kwarg(gridpath): with pytest.warns(FutureWarning): pc = uxgrid.to_polycollection(projection=ccrs.PlateCarree()) lc = uxgrid.to_linecollection(projection=ccrs.PlateCarree()) + + +def test_plot_edges(gridpath): + uxgrid = ux.open_grid(gridpath("ugrid", "outCSne30", "outCSne30.ug")) + fig, ax = plt.subplots(subplot_kw={'projection': ccrs.PlateCarree()}) + lines = uxgrid.plot_edges(ax=ax, color="green") + # check if all edges are plotted + assert len(lines) == uxgrid.n_edge + # check if the type is correct + assert all(isinstance(line, matplotlib.lines.Line2D) for line in lines) diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index b940912bb..4265b9fd6 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -2378,6 +2378,55 @@ def to_linecollection( return copy.deepcopy(line_collection) + def plot_edges( + self, + ax: Optional["cartopy.mpl.geoaxes.GeoAxes"] | None, # noqa: F821 + **plot_kwargs, + ): + """ + Plot the edges of the grid on a Cartopy map. + + Parameters + ---------- + ax : cartopy.mpl.geoaxes.GeoAxes or None + The axes on which to plot. If None, a new global map with PlatteCarree projection is used. + **plot_kwargs : dict + Additional keyword arguments passed to ``ax.plot`` (e.g., line style, + color, linewidth). + + Returns + ------- + list of matplotlib.lines.Line2D + """ + + import cartopy.crs as ccrs + import matplotlib.pyplot as plt + + if ax is None: + ax = plt.axes(projection=ccrs.PlateCarree()) + + vx, vy, vz = ax.projection.transform_points( + ccrs.PlateCarree(), self.node_lon, self.node_lat + ).T + + extent = ax.get_extent() + vmask = ( + (vx >= extent[0]) + * (vx <= extent[1]) + * (vy >= extent[2]) + * (vy <= extent[3]) + ) + emask = np.any(vmask[self.edge_node_connectivity], axis=-1) + edge_node_local = self.edge_node_connectivity.values[emask, :].T + + return ax.plot( + self.node_lon.values[edge_node_local], + self.node_lat.values[edge_node_local], + transform=ccrs.Geodetic(), + label=self.source_grid_spec, + **plot_kwargs, + ) + def get_dual(self, check_duplicate_nodes: bool = False): """Compute the dual for a grid, which constructs a new grid centered around the nodes, where the nodes of the primal become the face centers