Search code examples
pythonmatplotlibmapscartopy

Flexible map ticklables with cartopy


Quite regularly I find myself in the need of plotting (a lot of) maps of a variety of regions and region sizes. I would like these maps to have ticklabels indicating longitude and latitude (similar to this example: https://scitools.org.uk/cartopy/docs/v0.15/examples/tick_labels.html).

However, the solution suggested there does not work for me since it requires a priori knowledge about the region extend. I've written several way too complicated functions over the years in order to try and make this work in a flexible way. So what I'm wondering at this point: is there a simple solution to put latitude & longitude ticklabels to a map of variable extend?

This here comes somewhat close but is still quite unreliable:

import numpy as np
import cartopy.crs as ccrs 
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
import matplotlib.pyplot as plt


def auto_labeling(lon=np.linspace(-10, 40, 10), lat=np.linspace(30, 70, 10), filename='test1.png'):
    proj = ccrs.PlateCarree(central_longitude=0)
    data = np.ones((len(lon), len(lat)))
    ax = plt.subplot(projection=proj)
    ax.pcolormesh(lon, lat, data, transform=ccrs.PlateCarree(), alpha=.5)
    ax.coastlines()
    ax.set_xticks(ax.get_xticks(), crs=ccrs.PlateCarree())
    ax.set_yticks(ax.get_yticks(), crs=ccrs.PlateCarree())
    lon_formatter = LongitudeFormatter()
    lat_formatter = LatitudeFormatter()
    ax.xaxis.set_major_formatter(lon_formatter)
    ax.yaxis.set_major_formatter(lat_formatter)
    plt.savefig(filename, dpi=300)
    plt.close()


if __name__ == '__main__':
    auto_labeling(filename='test3.png')  # nice
    auto_labeling(np.linspace(-120, 120, 10), filename='test4.png')  # not nice but somewhat okay
    auto_labeling(np.linspace(-120, 120, 10), np.linspace(-70, 70, 10), filename='test5.png')  # nice
    # auto_labeling(np.linspace(-180, 180, 10), np.linspace(-90, 90, 10), filename='test6.png')  # fails

Solution

  • Okay the set_extend by @r-beginners set me on the right track. I still don't understand everything that is going on but two things seem to be important:

    • the automatically created ticks need to be restricted to [-180, 180] & [-90, 90] otherwise I also get an error
    • setting the extend makes the plot look nice for all cases I tried

    I've also added an offset parameter that deals with the issue that @swatchai raises and extents the range by half a grid cell in each direction by default. More is also possible as I think this looks nice sometimes.

    import numpy as np
    import cartopy.crs as ccrs
    from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
    import matplotlib.pyplot as plt
    
    
    def auto_labeling(lon, lat, filename, offset_dx=.5, offset_dy=.5):
        assert len(np.unique(lon[1:] - lon[:-1])) == 1
        assert len(np.unique(lat[1:] - lat[:-1])) == 1
        yy, xx = np.meshgrid(lat, lon)
        data = np.ones((len(lon), len(lat)))
    
        proj = ccrs.PlateCarree(central_longitude=0)
        ax = plt.subplot(projection=proj)
        ax.pcolormesh(xx, yy, data, transform=ccrs.PlateCarree(), alpha=.5)
        ax.coastlines()
    
        xticks = ax.get_xticks()
        yticks = ax.get_yticks()
        xticks = xticks[(xticks>=-180) & (xticks<=180)]
        yticks = yticks[(yticks>=-90) & (yticks<=90)]
        ax.set_xticks(xticks, crs=ccrs.PlateCarree())
        ax.set_yticks(yticks, crs=ccrs.PlateCarree())
    
        lon_formatter = LongitudeFormatter()
        lat_formatter = LatitudeFormatter()
        ax.xaxis.set_major_formatter(lon_formatter)
        ax.yaxis.set_major_formatter(lat_formatter)
    
        # set the plot extend
        dx = (lon[1] - lon[0])*offset_dx
        dy = (lat[1] - lat[0])*offset_dy
        lon_min = max([-180, min(lon) - dx])
        lon_max = min([180, max(lon) + dx])
        lat_min = max([-90, min(lat) - dy])
        lat_max = min([90, max(lat) + dy])
        ax.set_xlim(lon_min, lon_max)
        ax.set_ylim(lat_min, lat_max)
    
        plt.savefig(filename, dpi=300)
        plt.close()
    
    if __name__ == '__main__':
        auto_labeling(np.arange(-10, 40+2.5, 2.5), np.arange(30, 70+2.5, 2.5), 'test1.png', 1, 1)
        auto_labeling(np.arange(-120, 120+2.5, 2.5), np.arange(30, 70+2.5, 2.5), 'test2.png')
        auto_labeling(np.arange(-120, 120+2.5, 2.5), np.arange(-70, 70+2.5, 2.5), 'test3.png')
        auto_labeling(np.arange(-180+1.25, 180, 2.5), np.arange(-90+1.25, 90, 2.5), 'test4.png', 2, 3)  # offset is ignored for this case
    

    figure1 figure2 figure3 figure4