Skip to content

Commit

Permalink
Add emsarray.plot.add_landmarks(), landmarks parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
mx-moth committed Sep 21, 2023
1 parent cdedc1a commit 16f77f3
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/api/index.rst
Expand Up @@ -19,4 +19,5 @@ API reference
utils.rst
tutorial.rst
plot.rst
types.rst
exceptions.rst
9 changes: 9 additions & 0 deletions docs/api/types.rst
@@ -0,0 +1,9 @@
.. module:: emsarray.types

==============
emsarray.types
==============

.. automodule:: emsarray.types
:noindex:
:members:
1 change: 1 addition & 0 deletions docs/conf.py
Expand Up @@ -88,6 +88,7 @@

# Other documentation that we link to
intersphinx_mapping = {
'cartopy': ('https://scitools.org.uk/cartopy/docs/latest/', None),
'matplotlib': ('https://matplotlib.org/stable/', None),
'numpy': ('https://numpy.org/doc/stable/', None),
'pandas': ('https://pandas.pydata.org/docs/', None),
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/development.rst
Expand Up @@ -18,3 +18,6 @@ Next release (in development)
The old attribute was a compatibility shim around Shapely 1.8.x STRtree implementation.
Now that the minimum version of Shapely is 2.0, the STRtree can be used directly.
(:pr:`103`).
* Add :meth:`emsarray.plot.add_landmarks()`
and `landmarks` parameter to :meth:`Convention.plot` and related functions.
(:pr:`105`).
102 changes: 102 additions & 0 deletions src/emsarray/plot.py
Expand Up @@ -8,6 +8,7 @@
import numpy
import xarray

from emsarray.types import Landmark
from emsarray.utils import requires_extra

if TYPE_CHECKING:
Expand All @@ -20,6 +21,7 @@
from matplotlib import animation
from matplotlib.artist import Artist
from matplotlib.axes import Axes
from matplotlib import patheffects
from matplotlib.collections import PolyCollection
from matplotlib.figure import Figure
from shapely.geometry import Polygon
Expand Down Expand Up @@ -61,12 +63,101 @@ def add_coast(axes: Axes, **kwargs: Any) -> None:


def add_gridlines(axes: Axes) -> gridliner.Gridliner:
"""
Add some gridlines to the axes.
Parameters
----------
axes : :class:`matplotlib.axes.Axes`
The axes to add the gridlines to.
Returns
-------
cartopy.mpl.gridliner.Gridliner
"""
gridlines = axes.gridlines(draw_labels=True, auto_update=True)
gridlines.top_labels = False
gridlines.right_labels = False
return gridlines


def add_landmarks(
axes: Axes,
landmarks: Iterable[Landmark],
color: str = 'black',
outline_color: str = 'white',
outline_width: int = 2,
) -> None:
"""
Place some named landmarks on a plot.
Parameters
----------
axes : matplotlib.axes.Axes
The axes to add the landmarks to.
landmarks : list of :data:`landmarks <emsarray.types.Landmark>`
The landmarks to add. These are tuples of (name, point).
color : str, default 'black'
The color for the landmark marker and labels.
outline_color : str, default 'white'
The color for the outline.
Both the marker and the labels are outlined.
outline_width : ind, default 2
The linewidth of the outline.
Examples
--------
Draw a plot of a specific area with some landmarks:
.. code-block:: python
import emsarray.plot
import shapely
from matplotlib import pyplot
dataset = emsarray.tutorial.open_dataset('gbr4')
# Set up the figure
figure = pyplot.figure()
axes = figure.add_subplot(projection=dataset.ems.data_crs)
axes.set_title("Sea surface temperature around Mackay")
axes.set_aspect('equal', adjustable='datalim')
emsarray.plot.add_coast(axes, zorder=1)
# Focus on the area of interest
axes.set_extent((148.245710, 151.544167, -19.870197, -21.986412))
# Plot the temperature
temperature = dataset.ems.make_poly_collection(
dataset['temp'].isel(time=0, k=-1),
cmap='jet', edgecolor='face', zorder=0)
axes.add_collection(temperature)
figure.colorbar(temperature, label='°C')
# Name key locations
emsarray.plot.add_landmarks(axes, [
('The Percy Group', shapely.Point(150.270579, -21.658269)),
('Whitsundays', shapely.Point(148.955319, -20.169076)),
('Mackay', shapely.Point(149.192671, -21.146719)),
])
figure.show()
"""
outline = patheffects.withStroke(
linewidth=outline_width, foreground=outline_color)

points = axes.scatter(
[p.x for n, p in landmarks], [p.y for n, p in landmarks],
c=color, edgecolors=outline_color, linewidths=outline_width / 2)
points.set_path_effects([outline])

for name, point in landmarks:
text = axes.annotate(
name, (point.x, point.y),
textcoords='offset pixels', xytext=(10, -5))
text.set_path_effects([outline])


def bounds_to_extent(bounds: Tuple[float, float, float, float]) -> List[float]:
"""
Convert a Shapely bounds tuple to a matplotlib extents.
Expand Down Expand Up @@ -136,6 +227,7 @@ def plot_on_figure(
vector: Optional[Tuple[xarray.DataArray, xarray.DataArray]] = None,
title: Optional[str] = None,
projection: Optional[cartopy.crs.Projection] = None,
landmarks: Optional[Iterable[Landmark]] = None,
) -> None:
"""
Plot a :class:`~xarray.DataArray`
Expand All @@ -162,6 +254,8 @@ def plot_on_figure(
Optional, defaults to :class:`~cartopy.crs.PlateCarree`.
This is different to the coordinate reference system for the data,
which is defined in :attr:`.Convention.data_crs`.
landmarks : list of :data:`landmarks <emsarray.types.Landmark>`, optional
Landmarks to add to the plot. These are tuples of (name, point).
"""
if projection is None:
projection = cartopy.crs.PlateCarree()
Expand Down Expand Up @@ -192,6 +286,9 @@ def plot_on_figure(
if title:
axes.set_title(title)

if landmarks:
add_landmarks(axes, landmarks)

add_coast(axes)
add_gridlines(axes)
axes.autoscale()
Expand All @@ -207,6 +304,7 @@ def animate_on_figure(
vector: Optional[Tuple[xarray.DataArray, xarray.DataArray]] = None,
title: Optional[Union[str, Callable[[Any], str]]] = None,
projection: Optional[cartopy.crs.Projection] = None,
landmarks: Optional[Iterable[Landmark]] = None,
interval: int = 1000,
repeat: Union[bool, Literal['cycle', 'bounce']] = True,
) -> animation.FuncAnimation:
Expand Down Expand Up @@ -250,6 +348,8 @@ def animate_on_figure(
Optional, defaults to :class:`~cartopy.crs.PlateCarree`.
This is different to the coordinate reference system for the data,
which is defined in :attr:`.Convention.data_crs`.
landmarks : list of :data:`landmarks <emsarray.types.Landmark>`, optional
Landmarks to add to the plot. These are tuples of (name, point).
interval : int
The interval between frames of animation
repeat : {True, False, 'cycle', 'bounce'}
Expand Down Expand Up @@ -302,6 +402,8 @@ def animate_on_figure(
# Draw a coast overlay
add_coast(axes)
gridlines = add_gridlines(axes)
if landmarks:
add_landmarks(axes, landmarks)
axes.autoscale()

repeat_arg = True
Expand Down
15 changes: 14 additions & 1 deletion src/emsarray/types.py
@@ -1,7 +1,20 @@
"""Collection of type aliases used across the library."""
"""
A collection of descriptive type aliases used across the library.
"""

import os
from typing import Tuple, Union

import shapely


#: Something that can be used as a path.
Pathish = Union[os.PathLike, str]

#: Bounds of a geometry or of an area.
#: Components are ordered as (min x, min y, max x, max y).
Bounds = Tuple[float, float, float, float]

#: A landmark for a plot.
#: This is a tuple of the landmark name and and its location.
Landmark = Tuple[str, shapely.Point]

0 comments on commit 16f77f3

Please sign in to comment.