diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 376c4b9..e649de2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,13 +46,12 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.ref }} - name: Install Conda environment with Micromamba - uses: mamba-org/provision-with-micromamba@v14 + uses: mamba-org/setup-micromamba@v1 with: environment-file: environment.yml environment-name: DEVELOP - channels: conda-forge - cache-env: true - extra-specs: | + cache-environment: true + create-args: >- python=3.10 - name: Install package run: | @@ -140,14 +139,13 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.ref }} - name: Install Conda environment with Micromamba - uses: mamba-org/provision-with-micromamba@v12 + uses: mamba-org/setup-micromamba@v1 with: environment-file: environment${{ matrix.extra }}.yml environment-name: DEVELOP${{ matrix.extra }} - channels: conda-forge - cache-env: true - cache-env-key: ubuntu-latest-${{ matrix.python-version }}${{ matrix.extra }}. - extra-specs: | + cache-environment: true + cache-environment-key: ubuntu-latest-${{ matrix.python-version }}${{ matrix.extra }}. + create-args: >- python=${{matrix.python-version }} - name: Install package run: | diff --git a/src/earthkit/plots/components/subplots.py b/src/earthkit/plots/components/subplots.py index 376f2ae..397d7dc 100644 --- a/src/earthkit/plots/components/subplots.py +++ b/src/earthkit/plots/components/subplots.py @@ -878,7 +878,7 @@ def legend(self, style=None, location=None, **kwargs): dummy = [[1, 2], [3, 4]] mappable = self.contourf(x=dummy, y=dummy, z=dummy, style=style) layer = Layer(single.SingleSource(), mappable, self, style) - legend = layer.style.legend(layer, label=kwargs.pop("label", ""), **kwargs) + legend = layer.style.legend(layer, **kwargs) legends.append(legend) else: for i, layer in enumerate(self.distinct_legend_layers): diff --git a/src/earthkit/plots/geo/grids.py b/src/earthkit/plots/geo/grids.py index 1db379d..85f0ef7 100644 --- a/src/earthkit/plots/geo/grids.py +++ b/src/earthkit/plots/geo/grids.py @@ -21,59 +21,109 @@ _NO_SCIPY = True -def is_structured(lat, lon, tol=1e-5): +def is_structured(x, y, tol=1e-5): + """ + Determines whether the x and y points form a structured grid. + + This function checks if the x and y coordinate arrays represent a structured + grid, i.e., a grid with consistent spacing between points. The function supports + 1D arrays (representing coordinates of a grid) and 2D arrays (representing the + actual grid coordinates) of x and y. + + Parameters + ---------- + x : array_like + A 1D or 2D array of x-coordinates. For example, this can be longitude or + the x-coordinate in a Cartesian grid. + y : array_like + A 1D or 2D array of y-coordinates. For example, this can be latitude or + the y-coordinate in a Cartesian grid. + tol : float, optional + Tolerance for floating-point comparison to account for numerical precision + errors when checking spacing consistency. The default is 1e-5. + + Returns + ------- + bool + True if the data represents a structured grid, i.e., the spacing between + consecutive points in both x and y is consistent. False otherwise. """ - Determines whether the latitude and longitude points form a structured grid. - Parameters: - - lat: A 1D or 2D array of latitude points. - - lon: A 1D or 2D array of longitude points. - - tol: Tolerance for floating-point comparison (default 1e-5). + x = np.asarray(x) + y = np.asarray(y) - Returns: - - True if the data is structured (grid), False if it's unstructured. - """ + # If both x and y are 1D arrays, ensure they can form a grid + if x.ndim == 1 and y.ndim == 1: + # Check if the number of points match (can form a meshgrid) + if len(x) * len(y) != x.size * y.size: + return False + + # Check consistent spacing in x and y + x_diff = np.diff(x) + y_diff = np.diff(y) - lat = np.asarray(lat) - lon = np.asarray(lon) + x_spacing_consistent = np.all(np.abs(x_diff - x_diff[0]) < tol) + y_spacing_consistent = np.all(np.abs(y_diff - y_diff[0]) < tol) - # Check if there are consistent spacing in latitudes and longitudes - unique_lat = np.unique(lat) - unique_lon = np.unique(lon) + return x_spacing_consistent and y_spacing_consistent - # Structured grid condition: the number of unique lat/lon values should multiply to the number of total points - if len(unique_lat) * len(unique_lon) == len(lat) * len(lon): - # Now check if the spacing is consistent - lat_diff = np.diff(unique_lat) - lon_diff = np.diff(unique_lon) + # If x and y are 2D arrays, verify they are structured as a grid + elif x.ndim == 2 and y.ndim == 2: + # Check if rows of x and y have consistent spacing along the grid lines + # x should vary only along one axis, y along the other axis - # Check if lat/lon differences are consistent - lat_spacing_consistent = np.all(np.abs(lat_diff - lat_diff[0]) < tol) - lon_spacing_consistent = np.all(np.abs(lon_diff - lon_diff[0]) < tol) + x_rows_consistent = np.all( + np.abs(np.diff(x, axis=1) - np.diff(x, axis=1)[:, 0:1]) < tol + ) + y_columns_consistent = np.all( + np.abs(np.diff(y, axis=0) - np.diff(y, axis=0)[0:1, :]) < tol + ) - return lat_spacing_consistent and lon_spacing_consistent + return x_rows_consistent and y_columns_consistent - # If the product of unique lat/lon values doesn't match total points, it's unstructured - return False + else: + # Invalid input, dimensions of x and y must match (either both 1D or both 2D) + return False def interpolate_unstructured(x, y, z, resolution=1000, method="linear"): """ - Interpolates unstructured data to a structured grid, handling NaNs in z-values - and preventing interpolation across large gaps. - - Parameters: - - x: 1D array of x-coordinates. - - y: 1D array of y-coordinates. - - z: 1D array of z values. - - resolution: The number of points along each axis for the structured grid. - - method: Interpolation method ('linear', 'nearest', 'cubic'). - - gap_threshold: The distance threshold beyond which interpolation is not performed (set to NaN). - - Returns: - - grid_x: 2D grid of x-coordinates. - - grid_y: 2D grid of y-coordinates. - - grid_z: 2D grid of interpolated z-values, with NaNs in large gap regions. + Interpolate unstructured data to a structured grid. + + This function takes unstructured (scattered) data points and interpolates them + to a structured grid, handling NaN values in `z` and providing options for + different interpolation methods. It creates a regular grid based on the given + resolution and interpolates the z-values from the unstructured points onto this grid. + + Parameters + ---------- + x : array_like + 1D array of x-coordinates. + y : array_like + 1D array of y-coordinates. + z : array_like + 1D array of z-values at each (x, y) point. + resolution : int, optional + The number of points along each axis for the structured grid. + Default is 1000. + method : {'linear', 'nearest', 'cubic'}, optional + The interpolation method to use. Default is 'linear'. + The methods supported are: + + - 'linear': Linear interpolation between points. + - 'nearest': Nearest-neighbor interpolation. + - 'cubic': Cubic interpolation, which may produce smoother results. + + Returns + ------- + grid_x : ndarray + 2D array representing the x-coordinates of the structured grid. + grid_y : ndarray + 2D array representing the y-coordinates of the structured grid. + grid_z : ndarray + 2D array of interpolated z-values at the grid points. NaNs may be + present in regions where interpolation was not possible (e.g., due to + large gaps in the data). """ if _NO_SCIPY: raise ImportError( diff --git a/src/earthkit/plots/sources/earthkit.py b/src/earthkit/plots/sources/earthkit.py index 913f11b..e63916e 100644 --- a/src/earthkit/plots/sources/earthkit.py +++ b/src/earthkit/plots/sources/earthkit.py @@ -133,9 +133,17 @@ def extract_xy(self): ) points = get_points(1) else: - points = self.data.to_points(flatten=False) - x = points["x"] - y = points["y"] + try: + points = self.data.to_points(flatten=False) + x = points["x"] + y = points["y"] + except ValueError: + latlon = self.data.to_latlon(flatten=False) + lat = latlon["lat"] + lon = latlon["lon"] + transformed = self.crs.transform_points(ccrs.PlateCarree(), lon, lat) + x = transformed[:, :, 0] + y = transformed[:, :, 1] return x, y def extract_x(self): diff --git a/src/earthkit/plots/styles/legends.py b/src/earthkit/plots/styles/legends.py index e5a0106..19bef4a 100644 --- a/src/earthkit/plots/styles/legends.py +++ b/src/earthkit/plots/styles/legends.py @@ -13,7 +13,7 @@ # limitations under the License. -DEFAULT_LEGEND_LABEL = "" # {variable_name} ({units})" +DEFAULT_LEGEND_LABEL = "{variable_name} ({units})" _DISJOINT_LEGEND_LOCATIONS = { "bottom": { @@ -55,7 +55,10 @@ def colorbar(layer, *args, shrink=0.8, aspect=35, ax=None, **kwargs): Any keyword arguments accepted by `matplotlib.figures.Figure.colorbar`. """ label = kwargs.pop("label", DEFAULT_LEGEND_LABEL) - label = layer.format_string(label) + try: + label = layer.format_string(label) + except (AttributeError, ValueError, KeyError): + label = "" kwargs = {**layer.style._legend_kwargs, **kwargs} kwargs.setdefault("format", lambda x, _: f"{x:g}")