Skip to content

Instantly share code, notes, and snippets.

@jnhansen
Last active June 7, 2021 09:19
Show Gist options
  • Save jnhansen/eff9e011da86760a18f886eab9cc1d31 to your computer and use it in GitHub Desktop.
Save jnhansen/eff9e011da86760a18f886eab9cc1d31 to your computer and use it in GitHub Desktop.
This is a fix for cartopy `ax.gridlines()` that allows adding labels even for non-rectangular projections
import numpy as np
import cartopy
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
def gridlines_with_labels(ax, top=True, bottom=True, left=True,
right=True, **kwargs):
"""
Like :meth:`cartopy.mpl.geoaxes.GeoAxes.gridlines`, but will draw
gridline labels for arbitrary projections.
Parameters
----------
ax : :class:`cartopy.mpl.geoaxes.GeoAxes`
The :class:`GeoAxes` object to which to add the gridlines.
top, bottom, left, right : bool, optional
Whether or not to add gridline labels at the corresponding side
of the plot (default: all True).
kwargs : dict, optional
Extra keyword arguments to be passed to :meth:`ax.gridlines`.
Returns
-------
:class:`cartopy.mpl.gridliner.Gridliner`
The :class:`Gridliner` object resulting from ``ax.gridlines()``.
Example
-------
>>> import matplotlib.pyplot as plt
>>> import cartopy.crs as ccrs
>>> plt.figure(figsize=(10, 10))
>>> ax = plt.axes(projection=ccrs.Orthographic(-5, 53))
>>> ax.set_extent([-10.0, 0.0, 50.0, 56.0], crs=ccrs.PlateCarree())
>>> ax.coastlines('10m')
>>> gridlines_with_labels(ax)
>>> plt.show()
"""
# Add gridlines
gridliner = ax.gridlines(**kwargs)
ax.tick_params(length=0)
# Get projected extent
xmin, xmax, ymin, ymax = ax.get_extent()
# Determine tick positions
sides = {}
N = 500
if bottom:
sides['bottom'] = np.stack([np.linspace(xmin, xmax, N),
np.ones(N) * ymin])
if top:
sides['top'] = np.stack([np.linspace(xmin, xmax, N),
np.ones(N) * ymax])
if left:
sides['left'] = np.stack([np.ones(N) * xmin,
np.linspace(ymin, ymax, N)])
if right:
sides['right'] = np.stack([np.ones(N) * xmax,
np.linspace(ymin, ymax, N)])
# Get latitude and longitude coordinates of axes boundary at each side
# in discrete steps
gridline_coords = {}
for side, values in sides.items():
gridline_coords[side] = ccrs.PlateCarree().transform_points(
ax.projection, values[0], values[1])
lon_lim, lat_lim = gridliner._axes_domain(
background_patch=ax.background_patch)
ticklocs = {
'x': gridliner.xlocator.tick_values(lon_lim[0], lon_lim[1]),
'y': gridliner.ylocator.tick_values(lat_lim[0], lat_lim[1])
}
# Compute the positions on the outer boundary where
coords = {}
for name, g in gridline_coords.items():
if name in ('bottom', 'top'):
compare, axis = 'x', 0
else:
compare, axis = 'y', 1
coords[name] = np.array([
sides[name][:, np.argmin(np.abs(
gridline_coords[name][:, axis] - c))]
for c in ticklocs[compare]
])
# Create overlay axes for top and right tick labels
ax_topright = ax.figure.add_axes(ax.get_position(), frameon=False)
ax_topright.tick_params(
left=False, labelleft=False,
right=True, labelright=True,
bottom=False, labelbottom=False,
top=True, labeltop=True,
length=0
)
ax_topright.set_xlim(ax.get_xlim())
ax_topright.set_ylim(ax.get_ylim())
for side, tick_coords in coords.items():
if side in ('bottom', 'top'):
axis, idx = 'x', 0
else:
axis, idx = 'y', 1
_ax = ax if side in ('bottom', 'left') else ax_topright
ticks = tick_coords[:, idx]
valid = np.logical_and(
ticklocs[axis] >= gridline_coords[side][0, idx],
ticklocs[axis] <= gridline_coords[side][-1, idx])
if side in ('bottom', 'top'):
_ax.set_xticks(ticks[valid])
_ax.set_xticklabels([LONGITUDE_FORMATTER.format_data(t)
for t in ticklocs[axis][valid]])
else:
_ax.set_yticks(ticks[valid])
_ax.set_yticklabels([LATITUDE_FORMATTER.format_data(t)
for t in ticklocs[axis][valid]])
return gridliner
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@vinodkatmos
Copy link

Thanks for the solution. However, i get this issue:
TypeError: _axes_domain() got an unexpected keyword argument 'background_patch'

@jnhansen
Copy link
Author

jnhansen commented Apr 16, 2021

Thanks for the solution. However, i get this issue:
TypeError: _axes_domain() got an unexpected keyword argument 'background_patch'

@vinodkatmos I believe this workaround is now obsolete as Cartopy supports gridline labels for non-rectangular projections natively as of version 0.18.

@vinodkatmos
Copy link

I had to modify this is make it work for me (python 3.8, matplotlib 3.3.1, cartopy 0.18)

`import numpy as np
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER

def gridlines_with_labels(ax, top=True, bottom=True, left=True,
right=True, **kwargs):
"""
Parameters
----------
ax : :class:cartopy.mpl.geoaxes.GeoAxes
The :class:GeoAxes object to which to add the gridlines.
top, bottom, left, right : bool, optional
Whether or not to add gridline labels at the corresponding side
of the plot (default: all True).
kwargs : dict, optional
Extra keyword arguments to be passed to :meth:ax.gridlines.
Returns
-------
:class:cartopy.mpl.gridliner.Gridliner
The :class:Gridliner object resulting from ax.gridlines().
Example
-------
>>> import matplotlib.pyplot as plt
>>> import cartopy.crs as ccrs
>>> plt.figure(figsize=(10, 10))
>>> ax = plt.axes(projection=ccrs.Orthographic(-5, 53))
>>> ax.set_extent([-10.0, 0.0, 50.0, 56.0], crs=ccrs.PlateCarree())
>>> ax.coastlines('10m')
>>> gridlines_with_labels(ax)
>>> plt.show()
"""

# Add gridlines
gridliner = ax.gridlines(**kwargs)

ax.tick_params(length=0)

# Get projected extent
xmin, xmax, ymin, ymax = ax.get_extent()

# Determine tick positions
sides = {}
N = 500
if bottom:
    sides['bottom'] = np.stack([np.linspace(xmin, xmax, N),
                                np.ones(N) * ymin])
if top:
    sides['top'] = np.stack([np.linspace(xmin, xmax, N),
                            np.ones(N) * ymax])
if left:
    sides['left'] = np.stack([np.ones(N) * xmin,
                              np.linspace(ymin, ymax, N)])
if right:
    sides['right'] = np.stack([np.ones(N) * xmax,
                               np.linspace(ymin, ymax, N)])

# Get latitude and longitude coordinates of axes boundary at each side
# in discrete steps
gridline_coords = {}
for side, values in sides.items():
    gridline_coords[side] = ccrs.PlateCarree().transform_points(
        ax.projection, values[0], values[1])

lon_lim, lat_lim = gridliner._axes_domain()
ticklocs = {
    'x': gridliner.xlocator.tick_values(lon_lim[0], lon_lim[1]),
    'y': gridliner.ylocator.tick_values(lat_lim[0], lat_lim[1])
}

# Compute the positions on the outer boundary where
coords = {}
for name, g in gridline_coords.items():
    if name in ('bottom', 'top'):
        compare, axis = 'x', 0
    else:
        compare, axis = 'y', 1
    coords[name] = np.array([
        sides[name][:, np.argmin(np.abs(
            gridline_coords[name][:, axis] - c))]
        for c in ticklocs[compare]
    ])

# Create overlay axes for top and right tick labels
ax_topright = ax.figure.add_axes(ax.get_position(), frameon=False)
ax_topright.tick_params(
    left=False, labelleft=False,
    right=right, labelright=right,
    bottom=False, labelbottom=False,
    top=top, labeltop=top,
    length=0
)
ax_topright.set_xlim(ax.get_xlim())
ax_topright.set_ylim(ax.get_ylim())

for side, tick_coords in coords.items():
    if side in ('bottom', 'top'):
        axis, idx = 'x', 0
    else:
        axis, idx = 'y', 1

    _ax = ax if side in ('bottom', 'left') else ax_topright

    ticks = tick_coords[:, idx]

    valid = np.logical_and(
        ticklocs[axis] >= gridline_coords[side][0, idx],
        ticklocs[axis] <= gridline_coords[side][-1, idx])

    if side in ('bottom', 'top'):
        _ax.set_xticks(ticks[valid])
        _ax.set_xticklabels([LONGITUDE_FORMATTER.format_data(t)
                             for t in ticklocs[axis][valid]])
    else:
        _ax.set_yticks(ticks[valid])
        _ax.set_yticklabels([LATITUDE_FORMATTER.format_data(t)
                             for t in np.asarray(ticklocs[axis])[valid]])

return gridliner`

@jnhansen
Copy link
Author

@vinodkatmos For Cartopy 0.18, just use the built-in ax.gridlines(draw_labels=True) instead of this workaround.

@vinodkatmos
Copy link

@jnhansen Thanks a lot. The only thing i had to make sure is to specify crs=ccrs.PlateCarree() in ax.set_extent and remove crs=ccrs.Orthographic() in ax.gridlines(draw_labels=True).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment