Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge develop into main #27

Merged
merged 10 commits into from
Nov 17, 2024
16 changes: 7 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down
103 changes: 103 additions & 0 deletions docs/examples/gallery/interactive/meteogram.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies:
- pint
- pyyaml
- numpy
- plotly
- pip:
- adjustText
- earthkit-plots-default-styles
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
"cartopy>=0.22.0",
"pint",
"matplotlib",
"plotly",
"pyyaml",
"numpy",
"adjustText"
Expand Down
33 changes: 32 additions & 1 deletion src/earthkit/plots/_plugins.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
# Copyright 2024, European Centre for Medium Range Weather Forecasts.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from importlib.metadata import entry_points
from pathlib import Path


def register_plugins():
plugins = dict()
for plugin in entry_points(group="earthkit.plots.plugins"):

# Compatibility adjustment for Python 3.9 and earlier
all_entry_points = entry_points()

if hasattr(all_entry_points, "select"):
# For Python 3.10 and above
plugin_entry_points = all_entry_points.select(group="earthkit.plots.plugins")
else:
# For Python 3.9 and below, access the group directly from the dictionary
# and ensure it defaults to an empty list if not found
plugin_entry_points = all_entry_points.get("earthkit.plots.plugins", [])

# Additional handling for consistency in 3.9 by converting entry points if needed
if isinstance(plugin_entry_points, dict):
plugin_entry_points = plugin_entry_points.get("earthkit.plots.plugins", [])

for plugin in plugin_entry_points:
path = Path(plugin.load().__file__).parents[0]
plugins[plugin.name] = {
"identities": path / "identities",
Expand All @@ -14,6 +44,7 @@ def register_plugins():
for key, value in plugins[plugin.name].items():
if not value.exists():
plugins[plugin.name][key] = None

return plugins


Expand Down
37 changes: 21 additions & 16 deletions src/earthkit/plots/components/subplots.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,19 +374,6 @@ def _extract_plottables(
x_values = source.x_values
y_values = source.y_values

if method_name in (
"contour",
"contourf",
"pcolormesh",
) and not grids.is_structured(x_values, y_values):
x_values, y_values, z_values = grids.interpolate_unstructured(
x_values,
y_values,
z_values,
method=kwargs.pop("interpolation_method", "linear"),
)
extract_domain = False

if every is not None:
x_values = x_values[::every]
y_values = y_values[::every]
Expand All @@ -405,9 +392,23 @@ def _extract_plottables(
warnings.warn(
"The 'interpolation_method' argument is only valid for unstructured data."
)
mappable = getattr(style, method_name)(
self.ax, x_values, y_values, z_values, **kwargs
)
try:
mappable = getattr(style, method_name)(
self.ax, x_values, y_values, z_values, **kwargs
)
except TypeError as err:
if not grids.is_structured(x_values, y_values):
x_values, y_values, z_values = grids.interpolate_unstructured(
x_values,
y_values,
z_values,
method=kwargs.pop("interpolation_method", "linear"),
)
mappable = getattr(style, method_name)(
self.ax, x_values, y_values, z_values, **kwargs
)
else:
raise err
self.layers.append(Layer(source, mappable, self, style))
return mappable

Expand Down Expand Up @@ -947,6 +948,10 @@ def show(self):
"""Display the plot."""
return self.figure.show()

def save(self, *args, **kwargs):
"""Save the plot to a file."""
return self.figure.save(*args, **kwargs)


def thin_array(array, every=2):
"""
Expand Down
130 changes: 90 additions & 40 deletions src/earthkit/plots/geo/grids.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
19 changes: 19 additions & 0 deletions src/earthkit/plots/interactive/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2024, European Centre for Medium Range Weather Forecasts.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from earthkit.plots.interactive.charts import Chart

__all__ = [
"Chart",
]
23 changes: 23 additions & 0 deletions src/earthkit/plots/interactive/bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2024, European Centre for Medium Range Weather Forecasts.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import plotly.graph_objects as go

from earthkit.plots.interactive import inputs


@inputs.sanitise()
def bar(*args, **kwargs):
trace = go.Bar(*args, **kwargs)
return trace
Loading
Loading