• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ghiggi / gpm_api / 32078219827

17 Aug 2026 10:55PM UTC coverage: 91.068% (+0.5%) from 90.604%
32078219827

push

github

web-flow
[pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.9 → v0.16.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.9...v0.16.3)
- https://github.com/psf/black → https://github.com/psf/black-pre-commit-mirror
- [github.com/psf/black-pre-commit-mirror: 25.12.0 → 26.5.1](https://github.com/psf/black-pre-commit-mirror/compare/25.12.0...26.5.1)
- [github.com/codespell-project/codespell: v2.4.1 → v2.4.3](https://github.com/codespell-project/codespell/compare/v2.4.1...v2.4.3)
- [github.com/kynan/nbstripout: 0.8.1 → 0.9.1](https://github.com/kynan/nbstripout/compare/0.8.1...0.9.1)

16292 of 17890 relevant lines covered (91.07%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.56
/gpm/visualization/plot.py
1
# -----------------------------------------------------------------------------.
2
# MIT License
3

4
# Copyright (c) 2024 GPM-API developers
5
#
6
# This file is part of GPM-API.
7

8
# Permission is hereby granted, free of charge, to any person obtaining a copy
9
# of this software and associated documentation files (the "Software"), to deal
10
# in the Software without restriction, including without limitation the rights
11
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
# copies of the Software, and to permit persons to whom the Software is
13
# furnished to do so, subject to the following conditions:
14
#
15
# The above copyright notice and this permission notice shall be included in all
16
# copies or substantial portions of the Software.
17
#
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
# SOFTWARE.
25

26
# -----------------------------------------------------------------------------.
27
"""This module contains basic functions for GPM-API data visualization."""
28
import inspect
1✔
29
import warnings
1✔
30

31
import cartopy
1✔
32
import cartopy.crs as ccrs
1✔
33
import cartopy.feature as cfeature
1✔
34
import matplotlib.pyplot as plt
1✔
35
import numpy as np
1✔
36
import xarray as xr
1✔
37
from cartopy.mpl.gridliner import Gridliner
1✔
38
from pycolorbar import plot_colorbar, set_colorbar_fully_transparent
1✔
39
from pycolorbar.utils.mpl_legend import get_inset_bounds
1✔
40
from scipy.interpolate import griddata
1✔
41

42
import gpm
1✔
43
from gpm import get_plot_kwargs
1✔
44
from gpm.dataset.crs import compute_extent
1✔
45
from gpm.utils.area import get_lonlat_corners_from_centroids
1✔
46

47

48
def is_generator(obj):
1✔
49
    return inspect.isgeneratorfunction(obj) or inspect.isgenerator(obj)
1✔
50

51

52
def _call_optimize_layout(self):
1✔
53
    """Optimize the figure layout."""
54
    adapt_fig_size(ax=self.axes)
×
55
    self.figure.tight_layout()
×
56

57

58
def add_optimize_layout_method(p):
1✔
59
    """Add a method to optimize the figure layout using monkey patching."""
60
    p.optimize_layout = _call_optimize_layout.__get__(p, type(p))
1✔
61
    return p
1✔
62

63

64
def adapt_fig_size(ax, nrow=1, ncol=1):
1✔
65
    """Adjusts the figure height of the plot based on the aspect ratio of cartopy subplots.
66

67
    This function is intended to be called after all plotting has been completed.
68
    It operates under the assumption that all subplots within the figure share the same aspect ratio.
69

70
    Assumes that the first axis in the collection of axes is representative of all others.
71
    This means that all subplots are expected to have the same aspect ratio and size.
72

73
    The implementation is inspired by Mathias Hauser's mplotutils set_map_layout function.
74
    """
75
    # Determine the number of rows and columns of subplots in the figure.
76
    # This information is crucial for calculating the new height of the figure.
77
    # nrow, ncol, __, __ = ax.get_subplotspec().get_geometry()
78

79
    # Access the figure object from the axis to manipulate its properties.
80
    fig = ax.get_figure()
1✔
81

82
    # Retrieve the current size of the figure in inches.
83
    width, original_height = fig.get_size_inches()
1✔
84

85
    # A call to draw the canvas is required to make sure the geometry of the figure is up-to-date.
86
    # This ensures that subsequent calculations for adjusting the layout are based on the latest state.
87
    fig.canvas.draw()
1✔
88

89
    # Extract subplot parameters to understand the figure's layout.
90
    # These parameters include the margins of the figure and the spaces between subplots.
91
    bottom = fig.subplotpars.bottom
1✔
92
    top = fig.subplotpars.top
1✔
93
    left = fig.subplotpars.left
1✔
94
    right = fig.subplotpars.right
1✔
95
    hspace = fig.subplotpars.hspace  # vertical space between subplots
1✔
96
    wspace = fig.subplotpars.wspace  # horizontal space between subplots
1✔
97

98
    # Calculate the aspect ratio of the data in the subplot.
99
    # This ratio is used to adjust the height of the figure to match the aspect ratio of the data.
100
    aspect = ax.get_data_ratio()
1✔
101

102
    # Calculate the width of a single plot, considering the left and right margins,
103
    # the number of columns, and the space between columns.
104
    wp = (width - width * (left + (1 - right))) / (ncol + (ncol - 1) * wspace)
1✔
105

106
    # Calculate the height of a single plot using its width and the data aspect ratio.
107
    hp = wp * aspect
1✔
108

109
    # Calculate the new height of the figure, taking into account the number of rows,
110
    # the space between rows, and the top and bottom margins.
111
    height = (hp * (nrow + ((nrow - 1) * hspace))) / (1.0 - (bottom + (1 - top)))
1✔
112

113
    # Check if the new height is significantly reduced (more than halved).
114
    if original_height / height > 2:
1✔
115
        # Calculate the scale factor to adjust the figure size closer to the original.
116
        scale_factor = original_height / height / 2
×
117

118
        # Apply the scale factor to both width and height to maintain the aspect ratio.
119
        width *= scale_factor
×
120
        height *= scale_factor
×
121

122
    # Apply the calculated width and height to adjust the figure size.
123
    fig.set_figwidth(width)
1✔
124
    fig.set_figheight(height)
1✔
125

126

127
####--------------------------------------------------------------------------.
128

129

130
def infill_invalid_coords(xr_obj, x="lon", y="lat"):
1✔
131
    """Infill invalid coordinates.
132

133
    Interpolate the coordinates within the convex hull of data.
134
    Use nearest neighbour outside the convex hull of data.
135
    """
136
    # Copy object
137
    xr_obj = xr_obj.copy()
1✔
138
    lon = np.asanyarray(xr_obj[x].data)
1✔
139
    lat = np.asanyarray(xr_obj[y].data)
1✔
140
    # Retrieve infilled coordinates
141
    lon, lat, _ = get_valid_pcolormesh_inputs(x=lon, y=lat, data=None, mask_data=False)
1✔
142
    xr_obj[x].data = lon
1✔
143
    xr_obj[y].data = lat
1✔
144
    return xr_obj
1✔
145

146

147
def get_valid_pcolormesh_inputs(x, y, data, rgb=False, mask_data=True):
1✔
148
    """Infill invalid coordinates.
149

150
    Interpolate the coordinates within the convex hull of data.
151
    Use nearest neighbour outside the convex hull of data.
152

153
    This operation is required to plot with pcolormesh since it
154
    does not accept non-finite values in the coordinates.
155

156
    If  ``mask_data=True``, data values with invalid coordinates are masked
157
    and a numpy masked array is returned.
158
    Masked data values are not displayed in pcolormesh !
159
    If ``rgb=True``, it assumes the RGB dimension is the last data dimension.
160

161
    """
162
    # Retrieve mask of invalid coordinates
163
    x_invalid = ~np.isfinite(x)
1✔
164
    y_invalid = ~np.isfinite(y)
1✔
165
    mask = np.logical_or(x_invalid, y_invalid)
1✔
166

167
    # If no invalid coordinates, return original data
168
    if np.all(~mask):
1✔
169
        return x, y, data
1✔
170

171
    # Check at least ome valid coordinates
172
    if np.all(mask):
1✔
173
        raise ValueError("No valid coordinates.")
×
174

175
    # Mask the data
176
    if mask_data:
1✔
177
        if rgb:
1✔
178
            data_mask = np.broadcast_to(np.expand_dims(mask, axis=-1), data.shape)
1✔
179
            data_masked = np.ma.masked_where(data_mask, data)
1✔
180
        else:
181
            data_masked = np.ma.masked_where(mask, data)
1✔
182
    else:
183
        data_masked = data
×
184

185
    # Infill x and y
186
    # - Note: currently cause issue if NaN when crossing antimeridian ...
187
    # --> TODO: interpolation should be done in X,Y,Z
188
    if np.any(x_invalid):
1✔
189
        x = _interpolate_data(x, method="linear")  # interpolation
1✔
190
        x = _interpolate_data(x, method="nearest")  # nearest neighbours outside the convex hull
1✔
191
    if np.any(y_invalid):
1✔
192
        y = _interpolate_data(y, method="linear")  # interpolation
1✔
193
        y = _interpolate_data(y, method="nearest")  # nearest neighbours outside the convex hull
1✔
194
    return x, y, data_masked
1✔
195

196

197
def _interpolate_data(arr, method="linear"):
1✔
198
    # 1D coordinate (i.e. along_track/cross_track view)
199
    if arr.ndim == 1:
1✔
200
        return _interpolate_1d_coord(arr, method=method)
1✔
201
    # 2D coordinates (swath image)
202
    return _interpolate_2d_coord(arr, method=method)
1✔
203

204

205
def _interpolate_1d_coord(arr, method="linear"):
1✔
206
    # Find invalid locations
207
    is_invalid = ~np.isfinite(arr)
1✔
208

209
    # Find the indices of NaN values
210
    nan_indices = np.where(is_invalid)[0]
1✔
211

212
    # Return array if not NaN values
213
    if len(nan_indices) == 0:
1✔
214
        return arr
1✔
215

216
    # Find the indices of non-NaN values
217
    non_nan_indices = np.where(~is_invalid)
1✔
218

219
    # Create indices
220
    indices = np.arange(len(arr))
1✔
221

222
    # Points where we have valid data
223
    points = indices[non_nan_indices]
1✔
224

225
    # Points where data is NaN
226
    points_nan = indices[nan_indices]
1✔
227

228
    # Values at the non-NaN points
229
    values = arr[non_nan_indices]
1✔
230

231
    # Interpolate using griddata
232
    arr_new = arr.copy()
1✔
233
    arr_new[nan_indices] = griddata(points, values, points_nan, method=method)
1✔
234
    return arr_new
1✔
235

236

237
def _interpolate_2d_coord(arr, method="linear"):
1✔
238
    # Find invalid locations
239
    is_invalid = ~np.isfinite(arr)
1✔
240

241
    # Find the indices of NaN values
242
    nan_indices = np.where(is_invalid)
1✔
243

244
    # Return array if not NaN values
245
    if len(nan_indices) == 0:
1✔
246
        return arr
×
247

248
    # Find the indices of non-NaN values
249
    non_nan_indices = np.where(~is_invalid)
1✔
250

251
    # Create a meshgrid of indices
252
    x, y = np.meshgrid(range(arr.shape[1]), range(arr.shape[0]))
1✔
253

254
    # Points (X, Y) where we have valid data
255
    points = np.array([y[non_nan_indices], x[non_nan_indices]]).T
1✔
256

257
    # Points where data is NaN
258
    points_nan = np.array([y[nan_indices], x[nan_indices]]).T
1✔
259

260
    # Values at the non-NaN points
261
    values = arr[non_nan_indices]
1✔
262

263
    # Interpolate using griddata
264
    arr_new = arr.copy()
1✔
265
    arr_new[nan_indices] = griddata(points, values, points_nan, method=method)
1✔
266
    return arr_new
1✔
267

268

269
def _mask_antimeridian_crossing_arr(arr, antimeridian_mask, rgb):
1✔
270
    if np.ma.is_masked(arr):
1✔
271
        if rgb:
1✔
272
            antimeridian_mask = np.broadcast_to(np.expand_dims(antimeridian_mask, axis=-1), arr.shape)
1✔
273
            combined_mask = np.logical_or(arr.mask, antimeridian_mask)
1✔
274
        else:
275
            combined_mask = np.logical_or(arr.mask, antimeridian_mask)
1✔
276
        arr = np.ma.masked_where(combined_mask, arr)
1✔
277
    else:
278
        if rgb:
1✔
279
            antimeridian_mask = np.broadcast_to(
1✔
280
                np.expand_dims(antimeridian_mask, axis=-1),
281
                arr.shape,
282
            )
283
        arr = np.ma.masked_where(antimeridian_mask, arr)
1✔
284
    return arr
1✔
285

286

287
def mask_antimeridian_crossing_array(arr, lon, rgb, plot_kwargs):
1✔
288
    """Mask the array cells crossing the antimeridian.
289

290
    Here we assume not invalid lon coordinates anymore.
291
    Cartopy still bugs with several projections when data cross the antimeridian.
292
    By default, GPM-API mask data crossing the antimeridian.
293
    The GPM-API configuration default can be modified with: ``gpm.config.set({"viz_hide_antimeridian_data": False})``
294
    """
295
    antimeridian_mask = get_antimeridian_mask(lon)
1✔
296
    is_crossing_antimeridian = np.any(antimeridian_mask)
1✔
297
    if is_crossing_antimeridian:
1✔
298
        # Sanitize cmap to avoid cartopy bug related to cmap bad color
299
        # - Cartopy requires the bad color to be fully transparent
300
        plot_kwargs = _sanitize_cartopy_plot_kwargs(plot_kwargs)
1✔
301
        # Mask data based on GPM-API config 'viz_hide_antimeridian_data'
302
        if gpm.config.get("viz_hide_antimeridian_data"):  # default is True
1✔
303
            arr = _mask_antimeridian_crossing_arr(arr, antimeridian_mask=antimeridian_mask, rgb=rgb)
1✔
304
    return arr, plot_kwargs
1✔
305

306

307
def get_antimeridian_mask(lons):
1✔
308
    """Get mask of longitude coordinates neighbors crossing the antimeridian."""
309
    from scipy.ndimage import binary_dilation
1✔
310

311
    # Initialize mask
312
    n_y, n_x = lons.shape
1✔
313
    mask = np.zeros((n_y - 1, n_x - 1))
1✔
314
    # Check vertical edges
315
    row_idx, col_idx = np.where(np.abs(np.diff(lons, axis=0)) > 180)
1✔
316
    col_idx = np.clip(col_idx - 1, 0, n_x - 1)
1✔
317
    mask[row_idx, col_idx] = 1
1✔
318
    # Check horizontal edges
319
    row_idx, col_idx = np.where(np.abs(np.diff(lons, axis=1)) > 180)
1✔
320
    row_idx = np.clip(row_idx - 1, 0, n_y - 1)
1✔
321
    mask[row_idx, col_idx] = 1
1✔
322
    # Buffer by 1 in all directions to avoid plotting cells neighbour to those crossing the antimeridian
323
    # --> This should not be needed, but it's needed to avoid cartopy bugs !
324
    return binary_dilation(mask)
1✔
325

326

327
####--------------------------------------------------------------------------.
328
###########################
329
#### Cartopy utilities ####
330
###########################
331

332

333
def remove_bottom_gridlabels(ax):
1✔
334
    gridliners = [a for a in ax.artists if isinstance(a, Gridliner)]
×
335
    for gl in gridliners:
×
336
        gl.bottom_labels = False
×
337

338

339
def remove_left_gridlabels(ax):
1✔
340
    gridliners = [a for a in ax.artists if isinstance(a, Gridliner)]
×
341
    for gl in gridliners:
×
342
        gl.left_labels = False
×
343

344

345
####--------------------------------------------------------------------------.
346
########################
347
#### Plot utilities ####
348
########################
349

350

351
def preprocess_rgb_dataarray(da, rgb):
1✔
352
    if rgb:
1✔
353
        if rgb not in da.dims:
1✔
354
            raise ValueError(f"The specified rgb='{rgb}' must be a dimension of the DataArray.")
1✔
355
        if da[rgb].size not in [3, 4]:
1✔
356
            raise ValueError("The RGB dimension must have size 3 or 4.")
×
357
        da = da.transpose(..., rgb)
1✔
358
    return da
1✔
359

360

361
def check_object_format(da, plot_kwargs, check_function, **function_kwargs):
1✔
362
    """Check object format and valid dimension names."""
363
    # Preprocess RGB DataArrays
364
    da = da.squeeze()
1✔
365
    da = preprocess_rgb_dataarray(da, plot_kwargs.get("rgb", False))
1✔
366
    # Retrieve rgb or FacetGrid column/row dimensions
367
    dims_dict = {key: plot_kwargs.get(key) for key in ["rgb", "col", "row"] if plot_kwargs.get(key, None)}
1✔
368
    # Check such dimensions are available
369
    for key, dim in dims_dict.items():
1✔
370
        if dim not in da.dims:
1✔
371
            raise ValueError(f"The DataArray does not have a {key}='{dim}' dimension.")
1✔
372
    # Subset DataArray to check if complies with specific check function
373
    isel_dict = dict.fromkeys(dims_dict.values(), 0)
1✔
374
    check_function(da.isel(isel_dict), **function_kwargs)
1✔
375
    return da
1✔
376

377

378
def preprocess_figure_args(ax, fig_kwargs=None, subplot_kwargs=None, is_facetgrid=False):
1✔
379
    if is_facetgrid and ax is not None:
1✔
380
        raise ValueError("When plotting with FacetGrid, do not specify the 'ax'.")
1✔
381
    fig_kwargs = {} if fig_kwargs is None else fig_kwargs
1✔
382
    subplot_kwargs = {} if subplot_kwargs is None else subplot_kwargs
1✔
383
    if ax is not None:
1✔
384
        if len(subplot_kwargs) >= 1:
1✔
385
            raise ValueError("Provide `subplot_kwargs`only if ``ax``is None")
1✔
386
        if len(fig_kwargs) >= 1:
1✔
387
            raise ValueError("Provide `fig_kwargs` only if ``ax``is None")
1✔
388
    return fig_kwargs
1✔
389

390

391
def preprocess_subplot_kwargs(subplot_kwargs, infer_crs=False, xr_obj=None):
1✔
392
    subplot_kwargs = {} if subplot_kwargs is None else subplot_kwargs
1✔
393
    subplot_kwargs = subplot_kwargs.copy()
1✔
394
    if "projection" not in subplot_kwargs:
1✔
395
        if infer_crs:
1✔
396
            subplot_kwargs["projection"] = xr_obj.gpm.cartopy_crs
1✔
397
        else:
398
            subplot_kwargs["projection"] = ccrs.PlateCarree()
1✔
399
    return subplot_kwargs
1✔
400

401

402
def infer_xy_labels(da, x=None, y=None, rgb=None):
1✔
403
    from xarray.plot.utils import _infer_xy_labels
1✔
404

405
    # Infer dimensions
406
    x, y = _infer_xy_labels(da, x=x, y=y, imshow=True, rgb=rgb)  # dummy flag for rgb
1✔
407
    return x, y
1✔
408

409

410
def infer_map_xy_coords(da, x=None, y=None):
1✔
411
    """
412
    Infer possible map x and y coordinates for the given DataArray.
413

414
    Parameters
415
    ----------
416
    da : xarray.DataArray
417
        The input DataArray.
418
    x : str, optional
419
        The name of the x (i.e. longitude) coordinate. If None, it will be inferred.
420
    y : str, optional
421
        The name of the y (i.e. latitude) coordinate. If None, it will be inferred.
422

423
    Returns
424
    -------
425
    tuple
426
        The inferred (x, y) coordinates.
427
    """
428
    possible_x_coords = ["x", "lon", "longitude"]
1✔
429
    possible_y_coords = ["y", "lat", "latitude"]
1✔
430

431
    if x is None:
1✔
432
        for coord in possible_x_coords:
1✔
433
            if coord in da.coords:
1✔
434
                x = coord
1✔
435
                break
1✔
436
        else:
437
            raise ValueError("Cannot infer x coordinate. Please provide the x coordinate.")
×
438

439
    if y is None:
1✔
440
        for coord in possible_y_coords:
1✔
441
            if coord in da.coords:
1✔
442
                y = coord
1✔
443
                break
1✔
444
        else:
445
            raise ValueError("Cannot infer y coordinate. Please provide the y coordinate.")
×
446

447
    return x, y
1✔
448

449

450
def _get_proj_str(crs):
1✔
451
    with warnings.catch_warnings():
1✔
452
        warnings.simplefilter("ignore")
1✔
453
        proj_str = crs.to_dict().get("proj", "")
1✔
454
    return proj_str
1✔
455

456

457
def initialize_cartopy_plot(
1✔
458
    ax,
459
    fig_kwargs,
460
    subplot_kwargs,
461
    add_background,
462
    add_gridlines,
463
    add_labels,
464
    infer_crs=False,
465
    xr_obj=None,
466
):
467
    """Initialize figure for cartopy plot if necessary."""
468
    # - Initialize figure
469
    if ax is None:
1✔
470
        fig_kwargs = preprocess_figure_args(
1✔
471
            ax=ax,
472
            fig_kwargs=fig_kwargs,
473
            subplot_kwargs=subplot_kwargs,
474
        )
475
        subplot_kwargs = preprocess_subplot_kwargs(subplot_kwargs, infer_crs=infer_crs, xr_obj=xr_obj)
1✔
476
        _, ax = plt.subplots(subplot_kw=subplot_kwargs, **fig_kwargs)
1✔
477

478
    # - Add cartopy background
479
    if add_background:
1✔
480
        ax = plot_cartopy_background(ax)
1✔
481

482
    # - Add gridlines and labels
483
    if add_gridlines or add_labels:
1✔
484
        _ = plot_cartopy_gridlines_and_labels(ax, add_gridlines=add_gridlines, add_labels=add_labels)
1✔
485

486
    return ax
1✔
487

488

489
def plot_cartopy_gridlines_and_labels(ax, add_gridlines=True, add_labels=True):
1✔
490
    """Add cartopy gridlines and labels."""
491
    alpha = 0.1 if add_gridlines else 0
1✔
492
    gl = ax.gridlines(
1✔
493
        crs=ccrs.PlateCarree(),
494
        draw_labels=add_labels,
495
        linewidth=1,
496
        color="gray",
497
        alpha=alpha,
498
        linestyle="-",
499
    )
500
    gl.top_labels = False  # gl.xlabels_top = False
1✔
501
    gl.right_labels = False  # gl.ylabels_right = False
1✔
502
    gl.xlines = True
1✔
503
    gl.ylines = True
1✔
504
    return gl
1✔
505

506

507
def plot_cartopy_background(ax):
1✔
508
    """Plot cartopy background."""
509
    # - Add coastlines
510
    ax.coastlines()
1✔
511
    # - Add land and ocean
512
    # --> Raise error with some projections currently (shapely bug)
513
    # --> https://github.com/SciTools/cartopy/issues/2176
514
    if _get_proj_str(ax.projection) not in ["laea"]:
1✔
515
        ax.add_feature(cartopy.feature.LAND, facecolor=[0.9, 0.9, 0.9])
1✔
516
        ax.add_feature(cartopy.feature.OCEAN, alpha=0.6)
1✔
517
    # - Add borders
518
    ax.add_feature(cartopy.feature.BORDERS)  # BORDERS also draws provinces, ...
1✔
519
    return ax
1✔
520

521

522
def plot_sides(sides, ax, **plot_kwargs):
1✔
523
    """Plot boundary sides.
524

525
    Expects a list of (lon, lat) tuples.
526
    """
527
    for side in sides:
1✔
528
        p = ax.plot(*side, transform=ccrs.Geodetic(), **plot_kwargs)
1✔
529
    return p[0]
1✔
530

531

532
####--------------------------------------------------------------------------.
533
##########################
534
#### Cartopy wrappers ####
535
##########################
536

537

538
def _sanitize_cartopy_plot_kwargs(plot_kwargs):
1✔
539
    """Sanitize 'cmap' to avoid cartopy bug related to cmap bad color.
540

541
    Cartopy requires the bad color to be fully transparent.
542
    """
543
    cmap = plot_kwargs.get("cmap", None)
1✔
544
    if cmap is not None:
1✔
545
        bad = cmap.get_bad()
1✔
546
        bad[3] = 0  # enforce to 0 (transparent)
1✔
547
        cmap.set_extremes(bad=bad)
1✔
548
        plot_kwargs["cmap"] = cmap
1✔
549
    return plot_kwargs
1✔
550

551

552
def is_same_crs(crs1, crs2):
1✔
553
    """Check if same CRS."""
554
    with warnings.catch_warnings():
1✔
555
        warnings.simplefilter("ignore")
1✔
556
        crs1_dict = crs1.to_dict()
1✔
557
        crs2_dict = crs2.to_dict()
1✔
558
    keys = ["proj", "lat_0", "lon_0", "x_0", "y_0", "units", "type", "lon_wrap", "over", "pm"]
1✔
559
    dict1 = {key: crs1_dict.get(key) for key in keys}
1✔
560
    dict2 = {key: crs2_dict.get(key) for key in keys}
1✔
561
    return dict1 == dict2
1✔
562

563

564
def plot_cartopy_imshow(
1✔
565
    ax,
566
    da,
567
    x,
568
    y,
569
    interpolation="nearest",
570
    add_colorbar=True,
571
    plot_kwargs=None,
572
    cbar_kwargs=None,
573
):
574
    """Plot imshow with cartopy."""
575
    plot_kwargs = {} if plot_kwargs is None else plot_kwargs
1✔
576

577
    # Infer x and y
578
    x, y = infer_xy_labels(da, x=x, y=y, rgb=plot_kwargs.get("rgb", None))
1✔
579

580
    # Align x,y, data dimensions
581
    # - Ensure image with correct dimensions orders
582
    # - It can happen that x/y coords does not have same dimension order of data array.
583
    da = da.transpose(*da[y].dims, *da[x].dims, ...)
1✔
584

585
    # - Retrieve data
586
    arr = np.asanyarray(da.data)
1✔
587

588
    # - Compute coordinates
589
    x_coords = da[x].to_numpy()
1✔
590
    y_coords = da[y].to_numpy()
1✔
591

592
    # Compute extent
593
    extent = compute_extent(x_coords=x_coords, y_coords=y_coords)
1✔
594
    # area_extent = area_def.area_extent # [xmin, ymin, x_max, y_max]
595
    # extent = [area_extent[i] for i in [0, 2, 1, 3]] # [x_min, x_max, y_min, y_max]
596

597
    # Infer CRS of data, extent and cartopy projection
598
    try:
1✔
599
        crs = da.gpm.cartopy_crs
1✔
600
    except Exception:
×
601
        # Try assuming lon/lat CRS
602
        crs = ccrs.PlateCarree()
×
603

604
    # Determine image origin based on the orientation of da[y] values
605
    # - Cartopy assume origin is lower
606
    # - If y coordinate is increasing, set origin="lower"
607
    # - If y coordinate is decreasing, set origin="upper"
608
    #   --> Means that the image array is [::-1, :] reversed within cartopy
609
    y_increasing = y_coords[1] > y_coords[0]
1✔
610
    origin = "lower" if y_increasing else "upper"  # OLD CODE
1✔
611

612
    # Deal with decreasing y
613
    # if not y_increasing:  # decreasing y coordinates
614
    # extent = [extent[i] for i in [0, 1, 3, 2]]
615

616
    # Deal with out of limits x
617
    # - PlateeCarree coordinates out of bounds when  lons are defined as 0-360)
618
    set_extent = True
1✔
619

620
    # Case where coordinates are defined as 0-360 with pm=0
621
    if extent[1] > crs.x_limits[1] or extent[0] < crs.x_limits[0]:
1✔
622
        set_extent = False
×
623

624
    # Check if specify transform
625
    # - Specify transform argument only if data CRS is different from axes CRS
626
    # - If same crs, specifying transform is slower and might cuts away half of first and last row pixels
627
    # --> GPM-API automatically create the Cartopy GeoAxes with correct CRS
628
    transform = None if is_same_crs(crs, ax.projection) else crs
1✔
629

630
    # - Add variable field with cartopy
631
    rgb = plot_kwargs.pop("rgb", False)
1✔
632
    p = ax.imshow(
1✔
633
        arr,
634
        transform=transform,
635
        extent=extent,
636
        origin=origin,
637
        interpolation=interpolation,
638
        **plot_kwargs,
639
    )
640

641
    # - Set the extent
642
    # --> If some background is globally displayed, this zoom on the actual data region
643
    if set_extent:
1✔
644
        ax.set_extent(extent, crs=crs)
1✔
645

646
    # - Add colorbar
647
    if add_colorbar and not rgb:
1✔
648
        _ = plot_colorbar(p=p, ax=ax, **cbar_kwargs)
1✔
649
    return p
1✔
650

651

652
def plot_cartopy_pcolormesh(
1✔
653
    ax,
654
    da,
655
    x,
656
    y,
657
    add_colorbar=True,
658
    add_swath_lines=True,
659
    rasterized=True,
660
    plot_kwargs=None,
661
    cbar_kwargs=None,
662
):
663
    """Plot imshow with cartopy.
664

665
    x and y must represents longitude and latitudes.
666
    The function currently does not allow to zoom on regions across the antimeridian.
667
    The function mask scanning pixels which spans across the antimeridian.
668
    If the DataArray has a RGB dimension, plot_kwargs should contain the ``rgb``
669
    key with the name of the RGB dimension.
670

671
    """
672
    plot_kwargs = {} if plot_kwargs is None else plot_kwargs
1✔
673

674
    # Remove RGB from plot_kwargs
675
    rgb = plot_kwargs.pop("rgb", False)
1✔
676

677
    # Align x,y, data dimensions
678
    # - Ensure image with correct dimensions orders
679
    # - It can happen that x/y coords does not have same dimension order of data array.
680
    da = da.transpose(*da[y].dims, ...)
1✔
681

682
    # Get x, y, and array to plot
683
    da = preprocess_rgb_dataarray(da, rgb=rgb)
1✔
684
    da = da.compute()
1✔
685
    lon = da[x].data.copy()
1✔
686
    lat = da[y].data.copy()
1✔
687
    arr = da.data
1✔
688

689
    # Check if 1D coordinate (orbit nadir-view / transect / cross-section case)
690
    is_1d_case = lon.ndim == 1
1✔
691

692
    # Infill invalid value and mask data at invalid coordinates
693
    # - No invalid values after this function call
694
    lon, lat, arr = get_valid_pcolormesh_inputs(lon, lat, arr, rgb=rgb, mask_data=True)
1✔
695
    if is_1d_case:
1✔
696
        arr = np.expand_dims(arr, axis=1)
1✔
697

698
    # Ensure arguments
699
    if rgb:
1✔
700
        add_colorbar = False
1✔
701

702
    # Compute coordinates of cell corners for pcolormesh quadrilateral mesh
703
    # - This enable correct masking of cells crossing the antimeridian
704
    lon, lat = get_lonlat_corners_from_centroids(lon, lat, parallel=False)
1✔
705

706
    # Mask cells crossing the antimeridian
707
    # - with gpm.config.set({"viz_hide_antimeridian_data": False}): can be used to modify the masking behaviour
708
    arr, plot_kwargs = mask_antimeridian_crossing_array(arr, lon, rgb, plot_kwargs)
1✔
709

710
    # Add variable field with cartopy
711
    _ = plot_kwargs.setdefault("shading", "flat")
1✔
712
    p = ax.pcolormesh(
1✔
713
        lon,
714
        lat,
715
        arr,
716
        transform=ccrs.PlateCarree(),
717
        rasterized=rasterized,
718
        **plot_kwargs,
719
    )
720
    # Add swath lines
721
    # - TODO: currently assume that dimensions are (cross_track, along_track)
722
    if add_swath_lines and not is_1d_case:
1✔
723
        sides = [(lon[0, :], lat[0, :]), (lon[-1, :], lat[-1, :])]
1✔
724
        plot_sides(sides=sides, ax=ax, linestyle="--", color="black")
1✔
725

726
    # Add colorbar
727
    if add_colorbar:
1✔
728
        _ = plot_colorbar(p=p, ax=ax, **cbar_kwargs)
1✔
729
    return p
1✔
730

731

732
####-------------------------------------------------------------------------------.
733
#########################
734
#### Xarray wrappers ####
735
#########################
736

737

738
def _preprocess_xr_kwargs(add_colorbar, plot_kwargs, cbar_kwargs):
1✔
739
    if not add_colorbar:
1✔
740
        cbar_kwargs = None
1✔
741

742
    if "rgb" in plot_kwargs:
1✔
743
        cbar_kwargs = None
1✔
744
        add_colorbar = False
1✔
745
        args_to_keep = ["rgb", "col", "row", "origin"]  # alpha currently skipped if RGB
1✔
746
        plot_kwargs = {k: plot_kwargs[k] for k in args_to_keep if plot_kwargs.get(k, None) is not None}
1✔
747
    return add_colorbar, plot_kwargs, cbar_kwargs
1✔
748

749

750
def plot_xr_pcolormesh(
1✔
751
    ax,
752
    da,
753
    x,
754
    y,
755
    add_colorbar=True,
756
    cbar_kwargs=None,
757
    **plot_kwargs,
758
):
759
    """Plot pcolormesh with xarray."""
760
    is_facetgrid = bool("col" in plot_kwargs or "row" in plot_kwargs)
1✔
761
    ticklabels = cbar_kwargs.pop("ticklabels", None)
1✔
762
    add_colorbar, plot_kwargs, cbar_kwargs = _preprocess_xr_kwargs(
1✔
763
        add_colorbar=add_colorbar,
764
        plot_kwargs=plot_kwargs,
765
        cbar_kwargs=cbar_kwargs,
766
    )
767
    p = da.plot.pcolormesh(
1✔
768
        x=x,
769
        y=y,
770
        ax=ax,
771
        add_colorbar=add_colorbar,
772
        cbar_kwargs=cbar_kwargs,
773
        **plot_kwargs,
774
    )
775

776
    # Add variable name as title (if not FacetGrid)
777
    if not is_facetgrid:
1✔
778
        p.axes.set_title(da.name)
1✔
779

780
    if add_colorbar and ticklabels is not None:
1✔
781
        p.colorbar.ax.set_yticklabels(ticklabels)
×
782
    return p
1✔
783

784

785
def plot_xr_imshow(
1✔
786
    ax,
787
    da,
788
    x,
789
    y,
790
    interpolation="nearest",
791
    add_colorbar=True,
792
    add_labels=True,
793
    cbar_kwargs=None,
794
    visible_colorbar=True,
795
    **plot_kwargs,
796
):
797
    """Plot imshow with xarray.
798

799
    The colorbar is added with xarray to enable to display multiple colorbars
800
    when calling this function multiple times on different fields with
801
    different colorbars.
802
    """
803
    is_facetgrid = bool("col" in plot_kwargs or "row" in plot_kwargs)
1✔
804
    ticklabels = cbar_kwargs.pop("ticklabels", None)
1✔
805
    add_colorbar, plot_kwargs, cbar_kwargs = _preprocess_xr_kwargs(
1✔
806
        add_colorbar=add_colorbar,
807
        plot_kwargs=plot_kwargs,
808
        cbar_kwargs=cbar_kwargs,
809
    )
810
    # Allow using coords as x/y axis
811
    # BUG - Current bug in xarray
812
    if plot_kwargs.get("rgb", None) is not None:
1✔
813
        if x not in da.dims:
1✔
814
            da = da.swap_dims({list(da[x].dims)[0]: x})
×
815
        if y not in da.dims:
1✔
816
            da = da.swap_dims({list(da[y].dims)[0]: y})
×
817

818
    p = da.plot.imshow(
1✔
819
        x=x,
820
        y=y,
821
        ax=ax,
822
        interpolation=interpolation,
823
        add_colorbar=add_colorbar,
824
        add_labels=add_labels,
825
        cbar_kwargs=cbar_kwargs,
826
        **plot_kwargs,
827
    )
828

829
    # Add variable name as title (if not FacetGrid)
830
    if not is_facetgrid:
1✔
831
        p.axes.set_title(da.name)
1✔
832

833
    # Add colorbar ticklabels
834
    if add_colorbar and ticklabels is not None:
1✔
835
        p.colorbar.ax.set_yticklabels(ticklabels)
1✔
836

837
    # Make the colorbar fully transparent with a smart trick ;)
838
    # - TODO: this still cause issues when plotting 2 colorbars !
839
    if add_colorbar and not visible_colorbar:
1✔
840
        set_colorbar_fully_transparent(p)
1✔
841

842
    # Add manually the colorbar
843
    # p = da.plot.imshow(
844
    #     x=x,
845
    #     y=y,
846
    #     ax=ax,
847
    #     interpolation=interpolation,
848
    #     add_colorbar=False,
849
    #     **plot_kwargs,
850
    # )
851
    # plt.title(da.name)
852
    # if add_colorbar:
853
    #     _ = plot_colorbar(p=p, ax=ax, **cbar_kwargs)
854
    return p
1✔
855

856

857
####--------------------------------------------------------------------------.
858
####################
859
#### Plot Image ####
860
####################
861

862

863
def _plot_image(
1✔
864
    da,
865
    x=None,
866
    y=None,
867
    ax=None,
868
    add_colorbar=True,
869
    add_labels=True,
870
    interpolation="nearest",
871
    fig_kwargs=None,
872
    cbar_kwargs=None,
873
    **plot_kwargs,
874
):
875
    """Plot GPM orbit granule as in image."""
876
    from gpm.checks import is_grid, is_orbit
1✔
877
    from gpm.visualization.facetgrid import sanitize_facetgrid_plot_kwargs
1✔
878

879
    fig_kwargs = preprocess_figure_args(ax=ax, fig_kwargs=fig_kwargs)
1✔
880

881
    # - Initialize figure
882
    if ax is None:
1✔
883
        _, ax = plt.subplots(**fig_kwargs)
1✔
884

885
    # - Sanitize plot_kwargs set by by xarray FacetGrid.map_dataarray
886
    is_facetgrid = plot_kwargs.get("_is_facetgrid", False)
1✔
887
    plot_kwargs = sanitize_facetgrid_plot_kwargs(plot_kwargs)
1✔
888

889
    # - If not specified, retrieve/update plot_kwargs and cbar_kwargs as function of product name
890
    plot_kwargs, cbar_kwargs = get_plot_kwargs(
1✔
891
        name=da.name,
892
        user_plot_kwargs=plot_kwargs,
893
        user_cbar_kwargs=cbar_kwargs,
894
    )
895

896
    # Define x and y
897
    x, y = infer_xy_labels(da=da, x=x, y=y, rgb=plot_kwargs.get("rgb", None))
1✔
898

899
    # - Plot with xarray
900
    p = plot_xr_imshow(
1✔
901
        ax=ax,
902
        da=da,
903
        x=x,
904
        y=y,
905
        interpolation=interpolation,
906
        add_colorbar=add_colorbar,
907
        add_labels=add_labels,
908
        cbar_kwargs=cbar_kwargs,
909
        **plot_kwargs,
910
    )
911

912
    # Add custom labels
913
    default_labels = {
1✔
914
        "orbit": {"along_track": "Along-Track", "x": "Along-Track", "cross_track": "Cross-Track", "y": "Cross-Track"},
915
        "grid": {
916
            "lon": "Longitude",
917
            "longitude": "Longitude",
918
            "x": "Longitude",
919
            "lat": "Latitude",
920
            "latitude": "Latitude",
921
            "y": "Latitude",
922
        },
923
    }
924

925
    if add_labels:
1✔
926
        if is_orbit(da):
1✔
927
            ax.set_xlabel(default_labels["orbit"].get(x, x))
1✔
928
            ax.set_ylabel(default_labels["orbit"].get(y, y))
1✔
929
        elif is_grid(da):
1✔
930
            ax.set_xlabel(default_labels["grid"].get(x, x))
1✔
931
            ax.set_ylabel(default_labels["grid"].get(y, y))
1✔
932

933
    # - Monkey patch the mappable instance to add optimize_layout
934
    if not is_facetgrid:
1✔
935
        p = add_optimize_layout_method(p)
1✔
936
    # - Return mappable
937
    return p
1✔
938

939

940
def _plot_image_facetgrid(
1✔
941
    da,
942
    x=None,
943
    y=None,
944
    ax=None,
945
    add_colorbar=True,
946
    add_labels=True,
947
    interpolation="nearest",
948
    fig_kwargs=None,
949
    cbar_kwargs=None,
950
    **plot_kwargs,
951
):
952
    """Plot 2D fields with FacetGrid."""
953
    from gpm.visualization.facetgrid import ImageFacetGrid
1✔
954

955
    # Check inputs
956
    fig_kwargs = preprocess_figure_args(ax=ax, fig_kwargs=fig_kwargs, is_facetgrid=True)
1✔
957

958
    # Retrieve GPM-API defaults cmap and cbar kwargs
959
    variable = da.name
1✔
960
    plot_kwargs, cbar_kwargs = get_plot_kwargs(
1✔
961
        name=variable,
962
        user_plot_kwargs=plot_kwargs,
963
        user_cbar_kwargs=cbar_kwargs,
964
    )
965

966
    # Disable colorbar if rgb
967
    # - Move this to pycolorbar !
968
    # - Also remove cmap, norm, vmin and vmax in plot_kwargs
969
    if plot_kwargs.get("rgb", False):
1✔
970
        add_colorbar = False
1✔
971
        cbar_kwargs = {}
1✔
972

973
    # Create FacetGrid
974
    fc = ImageFacetGrid(
1✔
975
        data=da.compute(),
976
        col=plot_kwargs.pop("col", None),
977
        row=plot_kwargs.pop("row", None),
978
        col_wrap=plot_kwargs.pop("col_wrap", None),
979
        axes_pad=plot_kwargs.pop("axes_pad", None),
980
        fig_kwargs=fig_kwargs,
981
        cbar_kwargs=cbar_kwargs,
982
        add_colorbar=add_colorbar,
983
        aspect=plot_kwargs.pop("aspect", False),
984
        facet_height=plot_kwargs.pop("facet_height", 3),
985
        facet_aspect=plot_kwargs.pop("facet_aspect", 1),
986
    )
987

988
    # Plot the maps
989
    fc = fc.map_dataarray(
1✔
990
        _plot_image,
991
        x=x,
992
        y=y,
993
        add_colorbar=False,
994
        add_labels=add_labels,
995
        interpolation=interpolation,
996
        cbar_kwargs=cbar_kwargs,
997
        **plot_kwargs,
998
    )
999

1000
    # Remove duplicated or all labels
1001
    fc.remove_duplicated_axis_labels()
1✔
1002

1003
    if not add_labels:
1✔
1004
        fc.remove_left_ticks_and_labels()
×
1005
        fc.remove_bottom_ticks_and_labels()
×
1006

1007
    # Add colorbar
1008
    if add_colorbar:
1✔
1009
        fc.add_colorbar(**cbar_kwargs)
1✔
1010

1011
    return fc
1✔
1012

1013

1014
def plot_image(
1✔
1015
    da,
1016
    x=None,
1017
    y=None,
1018
    ax=None,
1019
    add_colorbar=True,
1020
    add_labels=True,
1021
    interpolation="nearest",
1022
    fig_kwargs=None,
1023
    cbar_kwargs=None,
1024
    **plot_kwargs,
1025
):
1026
    """Plot data using imshow.
1027

1028
    Parameters
1029
    ----------
1030
    da : xarray.DataArray
1031
        xarray DataArray.
1032
    x : str, optional
1033
        X dimension name.
1034
        If ``None``, takes the second dimension.
1035
        The default is ``None``.
1036
    y : str, optional
1037
        Y dimension name.
1038
        If ``None``, takes the first dimension.
1039
        The default is ``None``.
1040
    ax : cartopy.mpl.geoaxes.GeoAxes, optional
1041
        The matplotlib axes where to plot the image.
1042
        If ``None``, a figure is initialized using the
1043
        specified ``fig_kwargs``.
1044
        The default is ``None``.
1045
    add_colorbar : bool, optional
1046
        Whether to add a colorbar. The default is ``True``.
1047
    add_labels : bool, optional
1048
        Whether to add labels to the plot. The default is ``True``.
1049
    interpolation : str, optional
1050
        Argument to be passed to imshow.
1051
        The default is ``"nearest"``.
1052
    fig_kwargs : dict, optional
1053
        Figure options to be passed to :py:class:`matplotlib.pyplot.subplots`.
1054
        The default is ``None``.
1055
        Only used if ``ax`` is ``None``.
1056
    subplot_kwargs : dict, optional
1057
        Subplot options to be passed to :py:class:`matplotlib.pyplot.subplots`.
1058
        The default is ``None``.
1059
        Only used if ```ax``` is ``None``.
1060
    cbar_kwargs : dict, optional
1061
        Colorbar options. The default is ``None``.
1062
    **plot_kwargs
1063
        Additional arguments to be passed to the plotting function.
1064
        Examples include ``cmap``, ``norm``, ``vmin``, ``vmax``, ``levels``, ...
1065
        For FacetGrid plots, specify ``row``, ``col`` and ``col_wrap``.
1066
        With ``rgb`` you can specify the name of the xarray.DataArray RGB dimension.
1067

1068

1069
    """
1070
    from gpm.checks import check_is_spatial_2d, is_spatial_2d
1✔
1071

1072
    # Plot orbit
1073
    if not is_spatial_2d(da, strict=False):
1✔
1074
        raise ValueError("Can not plot. It's not a spatial 2D object.")
1✔
1075

1076
    # Check inputs
1077
    da = check_object_format(da, plot_kwargs=plot_kwargs, check_function=check_is_spatial_2d, strict=True)
1✔
1078

1079
    # Plot FacetGrid with xarray imshow
1080
    if "col" in plot_kwargs or "row" in plot_kwargs:
1✔
1081
        p = _plot_image_facetgrid(
1✔
1082
            da=da,
1083
            x=x,
1084
            y=y,
1085
            ax=ax,
1086
            add_colorbar=add_colorbar,
1087
            add_labels=add_labels,
1088
            interpolation=interpolation,
1089
            fig_kwargs=fig_kwargs,
1090
            cbar_kwargs=cbar_kwargs,
1091
            **plot_kwargs,
1092
        )
1093
    # Plot with xarray imshow
1094
    else:
1095
        p = _plot_image(
1✔
1096
            da=da,
1097
            x=x,
1098
            y=y,
1099
            ax=ax,
1100
            add_colorbar=add_colorbar,
1101
            add_labels=add_labels,
1102
            interpolation=interpolation,
1103
            fig_kwargs=fig_kwargs,
1104
            cbar_kwargs=cbar_kwargs,
1105
            **plot_kwargs,
1106
        )
1107
    # Return mappable
1108
    return p
1✔
1109

1110

1111
####--------------------------------------------------------------------------.
1112
##################
1113
#### Plot map ####
1114
##################
1115

1116

1117
def plot_map(
1✔
1118
    da,
1119
    x=None,
1120
    y=None,
1121
    ax=None,
1122
    interpolation="nearest",  # used only for GPM grid objects
1123
    add_colorbar=True,
1124
    add_background=True,
1125
    add_labels=True,
1126
    add_gridlines=True,
1127
    add_swath_lines=True,  # used only for GPM orbit objects
1128
    fig_kwargs=None,
1129
    subplot_kwargs=None,
1130
    cbar_kwargs=None,
1131
    **plot_kwargs,
1132
):
1133
    """Plot data on a geographic map.
1134

1135
    Parameters
1136
    ----------
1137
    da : xarray.DataArray
1138
        xarray DataArray.
1139
    x : str, optional
1140
        Longitude coordinate name.
1141
        If ``None``, takes the second dimension.
1142
        The default is ``None``.
1143
    y : str, optional
1144
        Latitude coordinate name.
1145
        If ``None``, takes the first dimension.
1146
        The default is ``None``.
1147
    ax : cartopy.mpl.geoaxes.GeoAxes, optional
1148
        The cartopy GeoAxes where to plot the map.
1149
        If ``None``, a figure is initialized using the
1150
        specified ``fig_kwargs`` and ``subplot_kwargs``.
1151
        The default is ``None``.
1152
    add_colorbar : bool, optional
1153
        Whether to add a colorbar. The default is ``True``.
1154
    add_labels : bool, optional
1155
        Whether to add cartopy labels to the plot. The default is ``True``.
1156
    add_gridlines : bool, optional
1157
        Whether to add cartopy gridlines to the plot. The default is ``True``.
1158
    add_swath_lines : bool, optional
1159
        Whether to plot the swath sides with a dashed line. The default is ``True``.
1160
        This argument only applies for ORBIT objects.
1161
    add_background : bool, optional
1162
        Whether to add the map background. The default is ``True``.
1163
    interpolation : str, optional
1164
        Argument to be passed to :py:class:`matplotlib.axes.Axes.imshow`. Only applies for GRID objects.
1165
        The default is ``"nearest"``.
1166
    fig_kwargs : dict, optional
1167
        Figure options to be passed to `matplotlib.pyplot.subplots`.
1168
        The default is ``None``.
1169
        Only used if ``ax`` is ``None``.
1170
    subplot_kwargs : dict, optional
1171
        Dictionary of keyword arguments for :py:class:`matplotlib.pyplot.subplots`.
1172
        Must contain the Cartopy CRS ` ``projection`` key if specified.
1173
        The default is ``None``.
1174
        Only used if ``ax`` is ``None``.
1175
    cbar_kwargs : dict, optional
1176
        Colorbar options. The default is ``None``.
1177
    **plot_kwargs
1178
        Additional arguments to be passed to the plotting function.
1179
        Examples include ``cmap``, ``norm``, ``vmin``, ``vmax``, ``levels``, ...
1180
        For FacetGrid plots, specify ``row``, ``col`` and ``col_wrap``.
1181
        With ``rgb`` you can specify the name of the xarray.DataArray RGB dimension.
1182

1183

1184
    """
1185
    from gpm.checks import has_spatial_dim, is_grid, is_orbit, is_spatial_2d
1✔
1186
    from gpm.visualization.grid import plot_grid_map
1✔
1187
    from gpm.visualization.orbit import plot_orbit_map
1✔
1188

1189
    # Plot orbit
1190
    # - allow vertical or other dimensions for FacetGrid
1191
    # - allow to plot a swath of size 1 (i.e. nadir-looking)
1192
    if is_orbit(da) and has_spatial_dim(da):
1✔
1193
        p = plot_orbit_map(
1✔
1194
            da=da,
1195
            x=x,
1196
            y=y,
1197
            ax=ax,
1198
            add_colorbar=add_colorbar,
1199
            add_background=add_background,
1200
            add_gridlines=add_gridlines,
1201
            add_labels=add_labels,
1202
            add_swath_lines=add_swath_lines,
1203
            fig_kwargs=fig_kwargs,
1204
            subplot_kwargs=subplot_kwargs,
1205
            cbar_kwargs=cbar_kwargs,
1206
            **plot_kwargs,
1207
        )
1208
    # Plot grid
1209
    elif is_grid(da) and is_spatial_2d(da, strict=False):
1✔
1210
        p = plot_grid_map(
1✔
1211
            da=da,
1212
            x=x,
1213
            y=y,
1214
            ax=ax,
1215
            interpolation=interpolation,
1216
            add_colorbar=add_colorbar,
1217
            add_background=add_background,
1218
            add_gridlines=add_gridlines,
1219
            add_labels=add_labels,
1220
            fig_kwargs=fig_kwargs,
1221
            subplot_kwargs=subplot_kwargs,
1222
            cbar_kwargs=cbar_kwargs,
1223
            **plot_kwargs,
1224
        )
1225
    else:
1226
        raise ValueError("Can not plot. It's neither a GPM GRID or GPM ORBIT spatial 2D object.")
1✔
1227
    # Return mappable
1228
    return p
1✔
1229

1230

1231
def plot_map_mesh(
1✔
1232
    xr_obj,
1233
    x=None,
1234
    y=None,
1235
    ax=None,
1236
    edgecolors="k",
1237
    linewidth=0.1,
1238
    add_background=True,
1239
    add_gridlines=True,
1240
    add_labels=True,
1241
    fig_kwargs=None,
1242
    subplot_kwargs=None,
1243
    **plot_kwargs,
1244
):
1245
    from gpm.checks import is_grid, is_orbit
1✔
1246
    from gpm.visualization.grid import plot_grid_mesh
1✔
1247
    from gpm.visualization.orbit import plot_orbit_mesh
1✔
1248

1249
    # Plot orbit
1250
    if is_orbit(xr_obj):
1✔
1251
        x, y = infer_map_xy_coords(xr_obj, x=x, y=y)
1✔
1252
        p = plot_orbit_mesh(
1✔
1253
            da=xr_obj[y],
1254
            ax=ax,
1255
            x=x,
1256
            y=y,
1257
            edgecolors=edgecolors,
1258
            linewidth=linewidth,
1259
            add_background=add_background,
1260
            add_gridlines=add_gridlines,
1261
            add_labels=add_labels,
1262
            fig_kwargs=fig_kwargs,
1263
            subplot_kwargs=subplot_kwargs,
1264
            **plot_kwargs,
1265
        )
1266
    elif is_grid(xr_obj):
1✔
1267
        p = plot_grid_mesh(
1✔
1268
            xr_obj=xr_obj,
1269
            x=x,
1270
            y=y,
1271
            ax=ax,
1272
            edgecolors=edgecolors,
1273
            linewidth=linewidth,
1274
            add_background=add_background,
1275
            add_gridlines=add_gridlines,
1276
            add_labels=add_labels,
1277
            fig_kwargs=fig_kwargs,
1278
            subplot_kwargs=subplot_kwargs,
1279
            **plot_kwargs,
1280
        )
1281
    else:
1282
        raise ValueError("Can not plot. It's neither a GPM GRID or GPM ORBIT spatial object.")
×
1283
    # Return mappable
1284
    return p
1✔
1285

1286

1287
def plot_map_mesh_centroids(
1✔
1288
    xr_obj,
1289
    x=None,
1290
    y=None,
1291
    ax=None,
1292
    c="r",
1293
    s=1,
1294
    add_background=True,
1295
    add_gridlines=True,
1296
    add_labels=True,
1297
    fig_kwargs=None,
1298
    subplot_kwargs=None,
1299
    **plot_kwargs,
1300
):
1301
    """Plot GPM orbit granule mesh centroids in a cartographic map."""
1302
    from gpm.checks import is_grid, is_orbit
1✔
1303

1304
    # Initialize figure if necessary
1305
    ax = initialize_cartopy_plot(
1✔
1306
        ax=ax,
1307
        fig_kwargs=fig_kwargs,
1308
        subplot_kwargs=subplot_kwargs,
1309
        add_background=add_background,
1310
        add_gridlines=add_gridlines,
1311
        add_labels=add_labels,
1312
        infer_crs=True,
1313
        xr_obj=xr_obj,
1314
    )
1315

1316
    # Retrieve orbits lon, lat coordinates
1317
    if is_orbit(xr_obj):
1✔
1318
        x, y = infer_map_xy_coords(xr_obj, x=x, y=y)
1✔
1319

1320
    # Retrieve grid centroids mesh
1321
    if is_grid(xr_obj):
1✔
1322
        x, y = infer_xy_labels(xr_obj, x=x, y=y)
1✔
1323
        xr_obj = create_grid_mesh_data_array(xr_obj, x=x, y=y)
1✔
1324

1325
    # Extract numpy arrays
1326
    lon = xr_obj[x].to_numpy()
1✔
1327
    lat = xr_obj[y].to_numpy()
1✔
1328

1329
    # Plot centroids
1330
    p = ax.scatter(lon, lat, transform=ccrs.PlateCarree(), c=c, s=s, **plot_kwargs)
1✔
1331

1332
    # Return mappable
1333
    return p
1✔
1334

1335

1336
def create_grid_mesh_data_array(xr_obj, x, y):
1✔
1337
    """Create a 2D mesh coordinates DataArray.
1338

1339
    Takes as input the 1D coordinate arrays from an existing xarray.DataArray or xarray.Dataset object.
1340

1341
    The function creates a 2D grid (mesh) of x and y coordinates and initializes
1342
    the data values to NaN.
1343

1344
    Parameters
1345
    ----------
1346
    xr_obj : xarray.DataArray or xarray.Dataset
1347
        The input xarray object containing the 1D coordinate arrays.
1348
    x : str
1349
        The name of the x-coordinate in `xr_obj`.
1350
    y : str
1351
        The name of the y-coordinate in `xr_obj`.
1352

1353
    Returns
1354
    -------
1355
    da_mesh : xarray.DataArray
1356
        A 2D xarray.DataArray with mesh coordinates for `x` and `y`, and NaN values for data points.
1357

1358
    Notes
1359
    -----
1360
    The resulting xarray.DataArray has dimensions named 'y' and 'x', corresponding to the
1361
    y and x coordinates respectively.
1362
    The coordinate values are taken directly from the input 1D coordinate arrays,
1363
    and the data values are set to NaN.
1364

1365
    """
1366
    # Extract 1D coordinate arrays
1367
    x_coords = xr_obj[x].to_numpy()
1✔
1368
    y_coords = xr_obj[y].to_numpy()
1✔
1369

1370
    # Create 2D meshgrid for x and y coordinates
1371
    X, Y = np.meshgrid(x_coords, y_coords, indexing="xy")
1✔
1372

1373
    # Create a 2D array of NaN values with the same shape as the meshgrid
1374
    dummy_values = np.full(X.shape, np.nan)
1✔
1375

1376
    # Create a new DataArray with 2D coordinates and NaN values
1377
    return xr.DataArray(
1✔
1378
        dummy_values,
1379
        coords={x: (("y", "x"), X), y: (("y", "x"), Y)},
1380
        dims=("y", "x"),
1381
    )
1382

1383

1384
####--------------------------------------------------------------------------.
1385

1386

1387
def _plot_labels(
1✔
1388
    xr_obj,
1389
    label_name=None,
1390
    max_n_labels=50,
1391
    add_colorbar=True,
1392
    interpolation="nearest",
1393
    cmap="Paired",
1394
    fig_kwargs=None,
1395
    **plot_kwargs,
1396
):
1397
    """Plot labels.
1398

1399
    The maximum allowed number of labels to plot is 'max_n_labels'.
1400
    """
1401
    from ximage.labels.labels import get_label_indices, redefine_label_array
1✔
1402
    from ximage.labels.plot_labels import get_label_colorbar_settings
1✔
1403

1404
    from gpm.visualization.plot import plot_image
1✔
1405

1406
    if isinstance(xr_obj, xr.Dataset):
1✔
1407
        dataarray = xr_obj[label_name]
1✔
1408
    else:
1409
        dataarray = xr_obj[label_name] if label_name is not None else xr_obj
1✔
1410

1411
    dataarray = dataarray.compute()
1✔
1412
    label_indices = get_label_indices(dataarray)
1✔
1413
    n_labels = len(label_indices)
1✔
1414
    if add_colorbar and n_labels > max_n_labels:
1✔
1415
        msg = f"""The array currently contains {n_labels} labels
1✔
1416
        and 'max_n_labels' is set to {max_n_labels}. The colorbar is not displayed!"""
1417
        print(msg)
1✔
1418
        add_colorbar = False
1✔
1419
    # Relabel array from 1 to ... for plotting
1420
    dataarray = redefine_label_array(dataarray, label_indices=label_indices)
1✔
1421
    # Replace 0 with nan
1422
    dataarray = dataarray.where(dataarray > 0)
1✔
1423
    # Define appropriate colormap
1424
    default_plot_kwargs, cbar_kwargs = get_label_colorbar_settings(label_indices, cmap=cmap)
1✔
1425
    default_plot_kwargs.update(plot_kwargs)
1✔
1426
    # Plot image
1427
    return plot_image(
1✔
1428
        dataarray,
1429
        interpolation=interpolation,
1430
        add_colorbar=add_colorbar,
1431
        cbar_kwargs=cbar_kwargs,
1432
        fig_kwargs=fig_kwargs,
1433
        **default_plot_kwargs,
1434
    )
1435

1436

1437
def plot_labels(
1✔
1438
    obj,  # Dataset, DataArray or generator
1439
    label_name=None,
1440
    max_n_labels=50,
1441
    add_colorbar=True,
1442
    interpolation="nearest",
1443
    cmap="Paired",
1444
    fig_kwargs=None,
1445
    **plot_kwargs,
1446
):
1447
    if is_generator(obj):
1✔
1448
        for _, xr_obj in obj:  # label_id, xr_obj
1✔
1449
            p = _plot_labels(
1✔
1450
                xr_obj=xr_obj,
1451
                label_name=label_name,
1452
                max_n_labels=max_n_labels,
1453
                add_colorbar=add_colorbar,
1454
                interpolation=interpolation,
1455
                cmap=cmap,
1456
                fig_kwargs=fig_kwargs,
1457
                **plot_kwargs,
1458
            )
1459
            plt.show()
1✔
1460
    else:
1461
        p = _plot_labels(
1✔
1462
            xr_obj=obj,
1463
            label_name=label_name,
1464
            max_n_labels=max_n_labels,
1465
            add_colorbar=add_colorbar,
1466
            interpolation=interpolation,
1467
            cmap=cmap,
1468
            fig_kwargs=fig_kwargs,
1469
            **plot_kwargs,
1470
        )
1471
    return p
1✔
1472

1473

1474
def plot_patches(
1✔
1475
    patch_gen,
1476
    variable=None,
1477
    add_colorbar=True,
1478
    interpolation="nearest",
1479
    fig_kwargs=None,
1480
    cbar_kwargs=None,
1481
    **plot_kwargs,
1482
):
1483
    """Plot patches."""
1484
    from gpm.visualization.plot import plot_image
1✔
1485

1486
    # Plot patches
1487
    for _, xr_patch in patch_gen:  # label_id, xr_obj
1✔
1488
        if isinstance(xr_patch, xr.Dataset):
1✔
1489
            if variable is None:
1✔
1490
                raise ValueError("'variable' must be specified when plotting xarray.Dataset patches.")
1✔
1491
            xr_patch = xr_patch[variable]
1✔
1492
        try:
1✔
1493
            plot_image(
1✔
1494
                xr_patch,
1495
                interpolation=interpolation,
1496
                add_colorbar=add_colorbar,
1497
                fig_kwargs=fig_kwargs,
1498
                cbar_kwargs=cbar_kwargs,
1499
                **plot_kwargs,
1500
            )
1501
            plt.show()
1✔
1502
        except Exception:
1✔
1503
            pass
1✔
1504

1505

1506
####--------------------------------------------------------------------------.
1507

1508

1509
def add_map_inset(ax, loc="upper left", inset_height=0.2, projection=None, inside_figure=True, border_pad=0):
1✔
1510
    """Adds an inset map to a matplotlib axis using Cartopy, highlighting the extent of the main plot.
1511

1512
    This function creates a smaller map inset within a larger map plot to show a global view or
1513
    contextual location of the main plot's extent.
1514

1515
    It uses Cartopy for map projections and plotting, and it outlines the extent of the main plot
1516
    within the inset to provide geographical context.
1517

1518
    Parameters
1519
    ----------
1520
    ax : matplotlib.axes.Axes or cartopy.mpl.geoaxes.GeoAxes
1521
        The main matplotlib or cartopy axis object where the geographic data is plotted.
1522
    loc : str, optional
1523
        The location of the inset map within the main plot.
1524
        Options include ``'lower left'``, ``'lower right'``,
1525
        ``'upper left'``, and ``'upper right'``. The default is ``'upper left'``.
1526
    inset_height : float, optional
1527
        The size of the inset height, specified as a fraction of the figure's height.
1528
        For example, a value of 0.2 indicates that the inset's height will be 20% of the figure's height.
1529
        The aspect ratio (of the map inset) will govern the ``inset_width``.
1530
    inside_figure : bool, optional
1531
        Determines whether the inset is constrained to be fully inside the figure bounds. If ``True`` (default),
1532
        the inset is placed fully within the figure. If ``False``, the inset can extend beyond the figure's edges,
1533
        allowing for a half-outside placement.
1534
    projection: cartopy.crs.Projection, optional
1535
        A cartopy projection. If ``None``, am Orthographic projection centered on the extent center is used.
1536

1537
    Returns
1538
    -------
1539
    ax2 : cartopy.mpl.geoaxes.GeoAxes
1540
        The Cartopy GeoAxesSubplot object for the inset map.
1541

1542
    Notes
1543
    -----
1544
    The function adjusts the extent of the inset map based on the main plot's extent, adding a
1545
    slight padding for visual clarity. It then overlays a red outline indicating the main plot's
1546
    geographical extent.
1547

1548
    Examples
1549
    --------
1550
    >>> p = da.gpm.plot_map()
1551
    >>> add_map_inset(ax=p.axes, loc="upper left", inset_height=0.15)
1552

1553
    This example creates a main plot with a specified extent and adds an upper-left inset map
1554
    showing the global context of the main plot's extent.
1555

1556
    """
1557
    from shapely import Polygon
1✔
1558

1559
    from gpm.utils.geospatial import extend_geographic_extent
1✔
1560

1561
    # Retrieve map extent and bounds
1562
    extent = ax.get_extent()
1✔
1563
    extent = extend_geographic_extent(extent, padding=0.5)
1✔
1564
    bounds = [extent[i] for i in [0, 2, 1, 3]]
1✔
1565

1566
    # Create Cartopy Polygon
1567
    polygon = Polygon.from_bounds(*bounds)
1✔
1568

1569
    # Define Orthographic projection
1570
    if projection is None:
1✔
1571
        lon_min, lon_max, lat_min, lat_max = extent
1✔
1572
        projection = ccrs.Orthographic(
1✔
1573
            central_latitude=(lat_min + lat_max) / 2,
1574
            central_longitude=(lon_min + lon_max) / 2,
1575
        )
1576

1577
    # Define aspect ratio of the map inset
1578
    aspect_ratio = float(np.diff(projection.x_limits).item() / np.diff(projection.y_limits).item())
1✔
1579

1580
    # Define inset location relative to main plot (ax) in normalized units
1581
    # - Lower-left corner of inset Axes, and its width and height
1582
    # - [x0, y0, width, height]
1583
    inset_bounds = get_inset_bounds(
1✔
1584
        ax=ax,
1585
        loc=loc,
1586
        inset_height=inset_height,
1587
        inside_figure=inside_figure,
1588
        aspect_ratio=aspect_ratio,
1589
        border_pad=border_pad,
1590
    )
1591

1592
    ax2 = ax.inset_axes(
1✔
1593
        inset_bounds,
1594
        projection=projection,
1595
    )
1596

1597
    # Add global map
1598
    ax2.set_global()
1✔
1599
    ax2.add_feature(cfeature.LAND)
1✔
1600
    ax2.add_feature(cfeature.OCEAN)
1✔
1601

1602
    # Add extent polygon
1603
    _ = ax2.add_geometries(
1✔
1604
        [polygon],
1605
        ccrs.PlateCarree(),
1606
        facecolor="none",
1607
        edgecolor="red",
1608
        linewidth=0.3,
1609
    )
1610
    return ax2
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc