颜色条相对于地理轴的正确放置(cartopy)

Correct placement of colorbar relative to geo axes (cartopy)

使用 Cartopy,我想完全控制颜色条的去向。通常我通过获取当前轴位置作为基础然后为颜色条创建新轴来做到这一点。这适用于标准的 matplotlib 轴,但在使用 Cartopy 和 geo_axes 时效果不佳,因为这会扭曲轴。

所以,我的问题是:如何获得 geo_axes 的准确位置?

这是一个基于 Cartopy 文档的代码示例 http://scitools.org.uk/cartopy/docs/latest/matplotlib/advanced_plotting.html:

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import os
from netCDF4 import Dataset as netcdf_dataset
from cartopy import config

def main():
    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'][:]

    #my preferred way of creating plots (even if it is only one plot)
    ef, ax = plt.subplots(1,1,figsize=(10,5),subplot_kw={'projection': ccrs.PlateCarree()})
    ef.subplots_adjust(hspace=0,wspace=0,top=0.925,left=0.1)

    #get size and extent of axes:
    axpos = ax.get_position()
    pos_x = axpos.x0+axpos.width + 0.01# + 0.25*axpos.width
    pos_y = axpos.y0
    cax_width = 0.04
    cax_height = axpos.height
    #create new axes where the colorbar should go.
    #it should be next to the original axes and have the same height!
    pos_cax = ef.add_axes([pos_x,pos_y,cax_width,cax_height])

    im = ax.contourf(lons, lats, sst, 60, transform=ccrs.PlateCarree())

    ax.coastlines()

    plt.colorbar(im, cax=pos_cax)

    ax.coastlines(resolution='110m')
    ax.gridlines()
    ax.set_extent([-20, 60, 33, 63])

    #when using this line the positioning of the colorbar is correct,
    #but the image gets distorted.
    #when omitting this line, the positioning of the colorbar is wrong,
    #but the image is well represented (not distorted).
    ax.set_aspect('auto', adjustable=None)

    plt.savefig('sst_aspect.png')
    plt.close()



if __name__ == '__main__': main()

使用 "set_aspect" 时的结果图:

结果图,当省略 "set_aspect" 时:

基本上,我想获得第一个数字(正确放置的颜色条)但不使用 "set_aspect"。 我想这应该可以通过一些转换来实现,但到目前为止我还没有找到解决方案。

谢谢!

好问题!感谢您的代码和图片,它使问题更容易理解,也更容易快速迭代可能的解决方案。

这里的问题本质上是一个 matplotlib 问题。 Cartopy 调用 ax.set_aspect('equal'),因为这是投影定义的笛卡尔单位的一部分。

Matplotlib 的等纵横比功能会调整轴的大小以匹配 x 和 y 限制,而不是更改限制以适合轴矩形。正是由于这个原因,轴没有填充图中分配给它的 space。如果您以交互方式调整图形大小,您会看到坐标轴占据的 space 数量根据您将图形大小调整到的方面而变化。

识别坐标轴位置的最简单方法是使用您已经使用过的 ax.get_position() 方法。然而,正如我们现在所知,这个 "position" 随着图形的大小而变化。因此,一种解决方案是每次调整图形大小时重新计算颜色条的位置。

matplotlib event machinery 有一个 "resize_event",每次调整图形大小时都会触发。如果我们将这种机器用于您的颜色条,我们的事件可能看起来像:

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)

因此,如果我们将此与 cartopy 以及您最初的问题联系起来,现在可以根据地理轴的位置调整颜色栏的大小。执行此操作的完整代码可能如下所示:

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()

请记住 mpl_toolkits.axes_grid1 不是 matplotlib 中经过最佳测试的部分,我们可以使用它的功能来实现您想要的。

我们可以使用mpl_toolkits文档中给出的Example,但是axes_class需要明确设置,必须设置为axes_class=plt.Axes,否则它试图创建一个 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)

另请注意使用 mpl_toolkits.axes_grid1 中的 AxesGrid 的 cartopy example

在上面非常有用的 的基础上,如果您不关心使用 plt.show 调整大小但在使用 plt.savefig 时正确调整大小和放置颜色条,我刚刚开发了这个代码片段,希望对其他人有用:

import wrf
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import cartopy.crs as ccrs

fig = plt.figure()
ax = plt.subplot(cart_proj) # cart_proj is from wrf.get_cartopy
ax.set_xlim(cart_xlim) # cart_xlim is from wrf.cartopy_xlim
ax.set_ylim(cart_ylim) # cart_ylim is from wrf.cartopy_ylim
ax.coastlines()
data_crs = ccrs.PlateCarree()

## Draw the contour plot (assume lons, lats, var are defined previously)
extend = 'max'
cmap = mpl.rainbow
bounds = np.arange(min_val, max_val, int_val) # preset these values to something meaningful
norm = mpl.colors.BoundaryNorm(bounds, cmap.N, extend=extend)
plt.contourf(wrf.to_np(lons), wrf.to_np(lats), wrf.to_np(var), bounds, cmap=cmap, norm=norm, extend=extend, transform=data_crs, transform_first=(ax,True))

## Create colorbar axes (temporarily) anywhere
cax = fig.add_axes([0,0,0.1,0.1])

## Find the location of the main plot axes
posn = ax.get_position()

## Where do you want the colorbar? 'bottom' or 'right' as examples
cbar_loc = 'bottom'
cbar_lab = 'Variable Name [units]'

## Adjust the positioning and orientation of the colorbar, and draw it
if cbar_loc == 'bottom':
   cax.set_position([posn.x0, posn.y0-0.09, posn.width, 0.05])
   plt.colorbar(cax=cax, orientation='horizontal', label=cbar_lab)
elif cbar_loc == 'right':
   cax.set_position([posn.x0+posn.width+0.05, posn.y0, 0.04, posn.height])
   plt.colorbar(cax=cax, orientation='vertical', label=cbar_lab)

## Save the figure (fname is set to whatever file path for the figure)
plt.savefig(fname)

颜色条不仅会正确定位并与 GeoAxes 框架的宽度或高度完全匹配(取决于您要放置它的位置),而且颜色条还会继承正确的 extend 属性以这种方式来自 plt.contourf,如果它和 norm 都是用 extend 属性定义的。我已经使用各种大小和宽高比的 WRF 域对此进行了测试,它似乎很可靠。