Skip to content
Draft
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
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ Grid
Grid.to_geodataframe
Grid.to_polycollection
Grid.to_linecollection
Grid.plot_edges
Grid.to_xarray

UxDataArray
Expand Down
32 changes: 32 additions & 0 deletions docs/user-guide/mpl.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
11 changes: 11 additions & 0 deletions test/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest
import numpy as np

import matplotlib
import matplotlib.pyplot as plt
import cartopy.crs as ccrs

Expand Down Expand Up @@ -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)
49 changes: 49 additions & 0 deletions uxarray/grid/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
Comment on lines +2381 to +2385
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned by @erogluorhan, if we could design this function similar to how we set up to_raster(), that would be preferred to avoid conflicting names with our .plot accessors.

to_raster also doesn't directly call any ax.plot routines. To be consistent with that, having this method return something that can then be externally passed into ax.plot would work.

Let me know if you have any ideas.

Copy link
Member

@erogluorhan erogluorhan Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like that, or I'd also be open to discussing whether to support matplotlib plotting functions from our plotting API with a clear distinction that they are not the default hvplot functions. Thinking out loud: Maybe through a newer accessor such as ux.mpl_plot, or through the existing one with clear function namings such as ux.plot.mpl_edges(), or something else. I am curious what you all would think about these.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I should admit that there might be some confusion with an ux.mpl_plot accessor or ux.plot.mpl_vis_funcs() because our current plotting functions through ux.plot accessor provide backend selection that enables hvplot to use either matplotlib or bokeh as the plotting engine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, we will need to think about all of these

"""
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
Expand Down