diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py index 15a0422d0..030309cd2 100644 --- a/lib/cartopy/mpl/geoaxes.py +++ b/lib/cartopy/mpl/geoaxes.py @@ -1685,6 +1685,46 @@ def scatter(self, *args, **kwargs): self.autoscale_view() return result + @_add_transform + def annotate(self, text, xy, xytext=None, xycoords='data', textcoords=None, + *args, **kwargs): + """ + Add the "transform" keyword to :func:`~matplotlib.pyplot.annotate`. + + Other Parameters + ---------------- + transform + A :class:`~cartopy.crs.Projection`. + + """ + transform = kwargs.pop('transform', None) + is_transform_crs = isinstance(transform, ccrs.CRS) + + # convert CRS to mpl transform for default 'data' setup + if is_transform_crs and xycoords == 'data': + xycoords = transform._as_mpl_transform(self) + + # textcoords = xycoords by default but complains if xytext is empty + if textcoords is None and xytext is not None: + textcoords = xycoords + + # use transform if textcoords is data and xytext is provided + if is_transform_crs and xytext is not None and textcoords == 'data': + textcoords = transform._as_mpl_transform(self) + + # convert to mpl_transform if CRS passed to xycoords + if isinstance(xycoords, ccrs.CRS): + xycoords = xycoords._as_mpl_transform(self) + + # convert to mpl_transform if CRS passed to textcoords + if isinstance(textcoords, ccrs.CRS): + textcoords = textcoords._as_mpl_transform(self) + + result = super().annotate(text, xy, xytext, xycoords=xycoords, + textcoords=textcoords, *args, **kwargs) + self.autoscale_view() + return result + @_add_transform def hexbin(self, x, y, *args, **kwargs): """ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_annotate.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_annotate.png new file mode 100644 index 000000000..5b2313d81 Binary files /dev/null and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_annotate.png differ diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py index 67eb77ba7..884a950f3 100644 --- a/lib/cartopy/tests/mpl/test_mpl_integration.py +++ b/lib/cartopy/tests/mpl/test_mpl_integration.py @@ -856,3 +856,70 @@ def test_streamplot(): ax.streamplot(x, y, u, v, transform=ccrs.PlateCarree(), density=(1.5, 2), color=mag, linewidth=2 * mag) return fig + + +@pytest.mark.natural_earth +@pytest.mark.mpl_image_compare() +def test_annotate(): + """ test a variety of annotate options on mulitple projections + + Annotate defaults to coords passed as if they're in map projection space. + `transform` or `xycoords` & `textcoords` control the marker and text offset + through shared or independent projections or coordinates. + `transform` is a cartopy kwarg so expects a CRS, + `xycoords` and `textcoords` accept CRS or matplotlib args. + + The various annotations below test a variety of the different combinations. + """ + # use IGH to test annotations cross projection splits and map boundaries + map_projection = ccrs.InterruptedGoodeHomolosine() + + fig = plt.figure(figsize=(10, 5)) + ax = fig.add_subplot(1, 1, 1, projection=map_projection) + ax.set_global() + ax.coastlines() + arrowprops = {'facecolor': 'red', + 'arrowstyle': "-|>", + 'connectionstyle': "arc3,rad=-0.2", + } + platecarree = ccrs.PlateCarree() + mpltransform = platecarree._as_mpl_transform(ax) + + # Add annotation with xycoords as mpltransform as suggested here + # https://stackoverflow.com/questions/25416600/why-the-annotate-worked-unexpected-here-in-cartopy/25421922#25421922 + ax.annotate('mpl xycoords', (-45, 43), xycoords=mpltransform, + size=5) + + # Add annotation with xycoords as projection + ax.annotate('crs xycoords', (-75, 13), xycoords=platecarree, + size=5) + + # set up coordinates in map projection space + map_coords = map_projection.transform_point(-175, -35, platecarree) + # Dont specifiy any args, default xycoords='data', transform=map projection + ax.annotate('default crs', map_coords, size=5) + + # data in map projection using default transform, with + # text positioned in platecaree transform + ax.annotate('mixed crs transforms', map_coords, xycoords='data', + xytext=(-175, -55), + textcoords=platecarree, + size=5, + arrowprops=arrowprops, + ) + + # Add annotation with point and text via transform + ax.annotate('crs transform', (-75, -20), xytext=(0, -55), + transform=platecarree, + arrowprops=arrowprops, + ) + + # Add annotation with point via transform and text non transformed + ax.annotate('offset textcoords', (-149.8, 61.22), transform=platecarree, + xytext=(-35, 10), textcoords='offset points', + size=5, + ha='right', + arrowprops=arrowprops, + ) + + return fig