Правильное размещение палитры относительно геоосей (картопия)

Используя 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". Я предполагаю, что это должно быть возможно с некоторыми преобразованиями, но пока я не нашел решения.

Спасибо!


person yngwaz    schedule 04.05.2015    source источник


Ответы (2)


Отличный вопрос! Спасибо за код и изображения, это значительно упрощает понимание проблемы, а также упрощает быстрый поиск возможных решений.

Проблема здесь, по сути, в matplotlib. Картопия вызывает ax.set_aspect('equal'), поскольку это часть декартовых единиц определения проекции.

Функция равного соотношения сторон Matplotlib изменяет размеры осей в соответствии с пределами x и y, а не изменяет пределы для соответствия прямоугольнику осей. Именно по этой причине оси не заполняют отведенное ей на рисунке место. Если вы в интерактивном режиме измените размер фигуры, вы увидите, что объем пространства, занимаемого осями, варьируется в зависимости от аспекта, на который вы изменяете размер фигуры.

Самый простой способ определить местоположение осей - это использовать метод ax.get_position(), который вы уже использовали. Однако, как мы теперь знаем, эта «позиция» меняется вместе с размером фигуры. Поэтому одним из решений является пересчет положения шкалы палитры каждый раз при изменении размера фигуры.

механизм обработки событий matplotlib имеет "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)

Итак, если мы свяжем это с картографией и вашим исходным вопросом, теперь можно изменить размер шкалы цветов в зависимости от положения геоосей. Полный код для этого может выглядеть так:

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()
person pelson    schedule 06.05.2015
comment
Отличный ответ! Я бы никогда не нашел это решение самостоятельно! - person yngwaz; 07.05.2015
comment
Может быть, небольшой побочный вопрос, если можно: можно ли поместить определение resize_colorbar где-нибудь еще и передать ax и cbar_ax в качестве аргументов (чтобы эта функция стала многоразовой)? - person yngwaz; 07.05.2015
comment
Обновление: в то время как plt.show () дает правильный результат, plt.savefig ('sst.png') - нет (цветовая полоса остается в исходной позиции в [0,0,0.1,0.1]). Пытался сменить бэкэнд на matplotlib.use ('Agg'), но это не помогает. Есть идеи, как заставить его работать с savefig? - person yngwaz; 07.05.2015
comment
Вопрос1: Как насчет функции, которая возвращает функцию resize_colorbar. ax и cbar_ax могут быть входом в эту функцию ... - person pelson; 17.05.2015
comment
Вопрос 2: Не проверено, но попробуйте просто позвонить resize_colorbar(None) перед сохранением. Я подозреваю, что при сохранении не запускается resize_event. - person pelson; 17.05.2015
comment
Не повезло с Q1, но, что более важно, решение для Q2 работает !! Спасибо! - person yngwaz; 19.05.2015
comment
@pelson: Опечатка в верхнем примере кода axpos.height вместо posn.height. Его нельзя редактировать, потому что он меньше 6 символов ... - person mathause; 01.03.2016
comment
Вот решение для первого квартала. Разве все это не должно быть встроено в cartopy? Так что ax.colorbar() на GeoAxes делает cbar_ax с правильными расширениями и заботится об обновлениях? - person j08lue; 04.04.2016

Принимая во внимание, что mpl_toolkits.axes_grid1 не является самой протестированной частью matplotlib, мы можем использовать ее функциональные возможности для достижения того, что вы хотите.

Мы можем использовать Пример, приведенный в mpl_toolkits документации, но axes_class должен быть установлен явно, он должен быть установлен как axes_class=plt.Axes, иначе он попытается создать GeoAxes как панель цветов

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)

Панель цветов с make_axes_locatable

Также обратите внимание на пример, в котором используется AxesGrid из mpl_toolkits.axes_grid1.

person mathause    schedule 13.12.2017
comment
Цветовая шкала в этих решениях правильно сохраняется в файле png. - person Jelmer Mulder; 31.03.2021
comment
замечательно, спасибо. +1 - person umbe1987; 12.05.2021