Correct placement of colorbar relative to geo axes (cartopy)

前端 未结 2 615
执笔经年
执笔经年 2020-12-15 18:51

Using Cartopy, I would like to have full control of where my colorbar goes. Usually I do this by getting the current axes position as basis and then create new axes for the

相关标签:
2条回答
  • 2020-12-15 19:24

    Bearing in mind that mpl_toolkits.axes_grid1 is not be the best-tested part of matplotlib, we can use it's functionality to achieve what you want.

    We can use the Example given in the mpl_toolkits documentation, but the axes_class needs to be set explicitly, it has to be set as axes_class=plt.Axes, else it attempts to create a GeoAxes as colorbar

    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.axes_grid1 import make_axes_locatable
    
    def sample_data_3d(shape):
        """Returns `lons`, `lats`, and fake `data`
    
        adapted from:
        http://scitools.org.uk/cartopy/docs/v0.15/examples/axes_grid_basic.html
        """
    
    
        nlons, nlats = shape
        lats = np.linspace(-np.pi / 2, np.pi / 2, nlats)
        lons = np.linspace(0, 2 * np.pi, nlons)
        lons, lats = np.meshgrid(lons, lats)
        wave = 0.75 * (np.sin(2 * lats) ** 8) * np.cos(4 * lons)
        mean = 0.5 * np.cos(2 * lats) * ((np.sin(2 * lats)) ** 2 + 2)
    
        lats = np.rad2deg(lats)
        lons = np.rad2deg(lons)
        data = wave + mean
    
        return lons, lats, data
    
    
    # get data
    lons, lats, data = sample_data_3d((180, 90))
    
    
    # set up the plot
    proj = ccrs.PlateCarree()
    
    f, ax = plt.subplots(1, 1, subplot_kw=dict(projection=proj))
    h = ax.pcolormesh(lons, lats, data, transform=proj, cmap='RdBu')
    ax.coastlines()
    
    # following https://matplotlib.org/2.0.2/mpl_toolkits/axes_grid/users/overview.html#colorbar-whose-height-or-width-in-sync-with-the-master-axes
    # we need to set axes_class=plt.Axes, else it attempts to create
    # a GeoAxes as colorbar
    
    divider = make_axes_locatable(ax)
    ax_cb = divider.new_horizontal(size="5%", pad=0.1, axes_class=plt.Axes)
    
    
    f.add_axes(ax_cb)
    plt.colorbar(h, cax=ax_cb)
    

    Also note the cartopy example that uses AxesGrid from mpl_toolkits.axes_grid1.

    0 讨论(0)
  • 2020-12-15 19:27

    Great question! Thanks for the code, and pictures, it makes the problem a lot easier to understand as well as making it easier to quickly iterate on possible solutions.

    The problem here is essentially a matplotlib one. Cartopy calls ax.set_aspect('equal') as this is part of the the Cartesian units of a projection's definition.

    Matplotlib's equal aspect ratio functionality resizes the axes to match the x and y limits, rather than changing the limits to fit to the axes rectangle. It is for this reason that the axes does not fill the space allocated to it on the figure. If you interactively resize the figure you will see that the amount of space that the axes occupies varies depending on the aspect that you resize your figure to.

    The simplest way of identifying the location of an axes is with the ax.get_position() method you have already been using. However, as we now know, this "position" changes with the size of the figure. One solution therefore is to re-calculate the position of the colorbar each time the figure is resized.

    The matplotlib event machinery has a "resize_event" which is triggered each time a figure is resized. If we use this machinery for your colorbar, our event might look something like:

    def resize_colobar(event):
        # Tell matplotlib to re-draw everything, so that we can get
        # the correct location from get_position.
        plt.draw()
    
        posn = ax.get_position()
        colorbar_ax.set_position([posn.x0 + posn.width + 0.01, posn.y0,
                                 0.04, axpos.height])
    
    fig.canvas.mpl_connect('resize_event', resize_colobar)
    

    So if we relate this back to cartopy, and your original question, it is now possible to resize the colorbar based on the position of the geo-axes. The full code to do this might look like:

    import cartopy.crs as ccrs
    import matplotlib.pyplot as plt
    import os
    from netCDF4 import Dataset as netcdf_dataset
    from cartopy import config
    
    
    fname = os.path.join(config["repo_data_dir"],
                     'netcdf', 'HadISST1_SST_update.nc'
                     )
    dataset = netcdf_dataset(fname)
    sst = dataset.variables['sst'][0, :, :]
    lats = dataset.variables['lat'][:]
    lons = dataset.variables['lon'][:]
    
    fig, ax = plt.subplots(1, 1, figsize=(10,5),
                           subplot_kw={'projection': ccrs.PlateCarree()})
    
    # Add the colorbar axes anywhere in the figure. Its position will be
    # re-calculated at each figure resize. 
    cbar_ax = fig.add_axes([0, 0, 0.1, 0.1])
    
    fig.subplots_adjust(hspace=0, wspace=0, top=0.925, left=0.1)
    
    sst_contour = ax.contourf(lons, lats, sst, 60, transform=ccrs.PlateCarree())
    
    
    def resize_colobar(event):
        plt.draw()
    
        posn = ax.get_position()
        cbar_ax.set_position([posn.x0 + posn.width + 0.01, posn.y0,
                              0.04, posn.height])
    
    fig.canvas.mpl_connect('resize_event', resize_colobar)
    
    ax.coastlines()
    
    plt.colorbar(sst_contour, cax=cbar_ax)
    
    
    ax.gridlines()
    ax.set_extent([-20, 60, 33, 63])
    
    plt.show()
    
    0 讨论(0)
提交回复
热议问题