Skip to content

Commit

Permalink
Merge develop into main (#27)
Browse files Browse the repository at this point in the history
* 12 support non latlon grib grids (#16)

* Adds support for non-lat-lon GRIB data points

* Re-enable automatic legend titles with proper fallback if no metadata found

* QA tweaks

* Rollback version of micromamba used with unit tests to v12

* Testing latest micromamba

* Migrate to new setup-micromamba

* Improve levels auto_range with proper symmetry (#17)

* Improve levels auto_range with proper symmetry

* Ensure symmetry around arbitrary divergence point

* Removes obsolete math import

---------

Co-authored-by: James Varndell <[email protected]>

* Fix datetime metadata for xarray for single-time datasets (#18)

* Only check for unstructured grid if normal plotting fails

* Adds convience save function at subplot level

* Fix earthkit mutably messing up an xarray's longitude (#22)

* Fix dimensionless unit formatting (#19)

* Fix dimensionless unit formatting

* Update src/earthkit/plots/metadata/units.py

Co-authored-by: James Varndell <[email protected]>

---------

Co-authored-by: James Varndell <[email protected]>

* Adds plugin support for Python 3.9 (#23)

* Feature/interactive plots (#25)

* Moves interactive plots into earthkit.plots.interactive

* Adds plotly to requirements

* Moves interactive plots into earthkit.plots.interactive

* Remove empty notebook

* QA tweaks

* Adds default style to improve look and feel with >5 quantiles

* Adds default style to improve look and feel with >5 quantiles

* Adds docstrings

* Adds tests for interactive plots

* QA tweaks

* Feature/vertical plots (#26)

* Moves interactive plots into earthkit.plots.interactive

* Adds plotly to requirements

* Moves interactive plots into earthkit.plots.interactive

* Remove empty notebook

* QA tweaks

* Adds default style to improve look and feel with >5 quantiles

* Adds default style to improve look and feel with >5 quantiles

* Adds docstrings

* Adds tests for interactive plots

* QA tweaks

* Adds better support for interactive vertical plots

---------

Co-authored-by: Juniper Tyree <[email protected]>
  • Loading branch information
JamesVarndell and juntyr authored Nov 17, 2024
1 parent 5a684b7 commit 7de602f
Show file tree
Hide file tree
Showing 22 changed files with 1,149 additions and 92 deletions.
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

0 comments on commit 7de602f

Please sign in to comment.