Плотный макет с использованием Cartopy без аргумента w_pad? - PullRequest
0 голосов
/ 14 июля 2020

При использовании tight_layout с Картографией и несколькими подзаголовками результат не является плотным макетом. Пример:

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import cartopy.crs as ccrs
   
gs = gridspec.GridSpec(2,2)

fig = plt.figure()
for k in range(0,4):
    ax = plt.subplot(gs[k], projection = ccrs.PlateCarree())
    ax.set_extent([0,50,0,90])
    ax.coastlines()
gs.tight_layout(fig)

создает фигуру с большим промежутком между каждой картой:

Wide gap example

The same issue occurs when using pyplot's tight_layout, whether I set the ax.set_extent() or not, the result is not as expected. Example:

import matplotlib.pyplot as plt
import cartopy.crs as ccrs

fig, ((ax1,ax2),(ax3,ax4)) = plt.subplots(2, 2,
                             subplot_kw=dict(projection=ccrs.PlateCarree()))
for ax in fig.axes:
    ax.coastlines()
plt.tight_layout()

Overlapped example

While I can adjust this manually on both cases by using w_pad and h_pad arguments on tight_layout, I would have to do this manually for every figure depending on the amount of subplots and/or the map itself. Is there any way to do this without these arguments, or to automatically determine what these values should be? Or maybe automatically set the figure size?

Using Linux 5.4.0-40-generic, Python 3.7.6 64-bit, Cartopy 0.18.0, Matplotlib 3.2.2

Edit:

The question is how to reproduce the tight_layout function using Cartopy without needing to manually set the w_pad or h_pad depending on what I'm plotting.

Currently, I'm creating many different figures, changing:

  • Projections: some are PlateCarree, some are Polar Stereographic
  • Map extent: entire map, reduced lat/lon on PlateCarree, reduced latitudes on Polar Stereographic
  • Different amount of rows/cols: 1 to 7 rows, 1 to 5 columns.
  • Adding latitudes and longitudes ticks on the first column and the last row, a title to each column, an y_label on the first column, and an overall title.
  • Depending on the figure, adding a colorbar on the right of each row.

The end result should be figures that have consistent horizontal and vertical space between each subplot and the different figures, like the expected result of tight_layout.

Example code with all the above, following @swatchai answer:

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
from cartopy.examples.waves import sample_data
from mpl_toolkits.axes_grid1.inset_locator import inset_axes

ar = 1.0  # initial aspect ratio for first trial
wi = 5   # width in inches
hi = wi * ar  # height in inches

# set number of rows/columns
rows, cols = 2,2
# set projection
# proj = ccrs.PlateCarree()
proj = ccrs.NorthPolarStereo()
# set lon/lat extent
# extent = [0,50,0,90] # example for PlateCarree
# extent = [-180,180,-90,90] # complete map
extent = [-180,180,30,90] # example for PolarStereo

gs = gridspec.GridSpec(rows, cols)

# Set figsize using wi and hi
fig = plt.figure(figsize=(wi, hi))
for r in range(0,rows):
    for c in range(0,cols):
        ax = plt.subplot(gs[r,c], projection = proj)
        ax.set_extent(extent,ccrs.PlateCarree())
        ax.coastlines()
        
        # Add sample data
        x, y, z = sample_data((20, 40))
        z = z * -1.5 * y
        cf = ax.contourf(x, y, z, transform=ccrs.PlateCarree())
        
        # Add colorbar
        if c == cols -1:
            axins = inset_axes(ax,
                    bbox_to_anchor=(1.05, 0., 1, 1),
                    width='5%',height='98%',loc='lower left',
                    bbox_transform=ax.transAxes,
                    borderpad=0)
             
            cbar = fig.colorbar(cf, cax=axins,orientation='vertical')
           
        # Gridlines, labels, titles
        if r == 0:
            ax.set_title('column title')
        if c == 0:
            ax.set_ylabel('y label')
            ax.set_yticks([])
        if proj == ccrs.PlateCarree():
            gl = ax.gridlines(draw_labels=False, crs=ccrs.PlateCarree())
            gl.xformatter, gl.yformatter  = LONGITUDE_FORMATTER,  LATITUDE_FORMATTER
            if r == rows-1:
                gl.xlabels_bottom = True
            if c == 0:
                gl.ylabels_left = True 
                ax.set_ylabel('y label',labelpad=35)
                ax.set_yticks([])
        elif proj == ccrs.NorthPolarStereo():
            ax.gridlines()
plt.suptitle('A figure title')
        
        
# Do this to get updated positions/dimensions   
plt.draw() 

# # Get proper ratio here
# # Computed from the last `ax` (being the current)
xmin, xmax = ax.get_xbound()
ymin, ymax = ax.get_ybound()
y2x_ratio = (ymax-ymin)/(xmax-xmin) * rows/cols

# # Apply new h/w aspect ratio by changing h
# # Also possible to change w using set_figwidth()
fig.set_figheight(wi * y2x_ratio)

gs.tight_layout(fig)
plt.show()

The above produces graphs that have very different horizontal/vertical gaps depending on the amount of subplots/projections. The labels, titles, etc are also sometimes out of proportion for the overall figure (which doesn't happen when tight_layout works as intended). Some are also rotated. E.g., using the above code with rows, cols = 3, 4, PlateCarree() and the complete map plotted, gives:

Большие промежутки по горизонтали / вертикали, поворотная ось

1 Ответ

1 голос
/ 15 июля 2020

Чтобы получить лучшие графики, вы должны объявить figsize с правильной шириной и высотой таким образом, чтобы соотношение высота / ширина соответствовало размерам подзаголовков. Можно вычислить соотношение программно в коде, затем применить его как значение обновления и получить графики по мере необходимости.

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import cartopy.crs as ccrs

# use figsize=(w,h) of proper values can give good result
# the aspect ratio, h/w is important
# default is around 1, is good in general, but not in your case

ar = 1.0  # initial aspect ratio for first trial
wi = 5    # width in inches
hi = wi * ar  # height in inches

gs = gridspec.GridSpec(2,2)

# Set figsize using wi and hi
fig = plt.figure(figsize=(wi, hi))  

for k in range(0,4):
    ax = plt.subplot(gs[k], projection = ccrs.PlateCarree())  
    # any of these projections are OK to use
    # PlateCarree() Robinson() Mercator() TransverseMercator() Orthographic()
    ax.set_extent([0,50,0,90])
    ax.coastlines()
    ax.gridlines()

# Do this to get updated positions/dimensions   
plt.draw() 

# Get proper ratio here
xmin, xmax = ax.get_xbound()
ymin, ymax = ax.get_ybound()
y2x_ratio = (ymax-ymin)/(xmax-xmin)

# This also gives same ratio
#x1, x2 = ax.get_xlim()
#y1, y2 = ax.get_ylim()
#y2x_ratio = (y2-y1)/(x2-x1)

print("y2x_ratio: "+ str(y2x_ratio)) 
#1.8258 with Robinson; 1.8 PlateCarree; 3.37 Mercator; 1.549 TransverseMercator

# Apply new h/w aspect ratio by changing h
# Also possible to change w using set_figwidth()
fig.set_figheight(wi * y2x_ratio)

gs.tight_layout(fig)
plt.show()

График вывода:

2x2maps

Edit 1

The original answer concentrates on an array of 2x2 (rows x columns). For other array, the computation of aspect ratio must be changed to account for it. Here is an update version of runnable code and sample plot.

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import cartopy.crs as ccrs

# use figsize=(w,h) of proper values can give good result
# the aspect ratio, h/w is important
# default is around 1, is good in general cases

ar = 1.0  # initial aspect ratio for first trial
wi = 5    # width of the whole figure in inches, ...
# set it wide enough to cover all columns of sub-plots

hi = wi * ar  # height in inches

# set number of rows/columns
rows, cols = 4,3

gs = gridspec.GridSpec(rows, cols)

# Set figsize using wi and hi
fig = plt.figure(figsize=(wi, hi))

for k in range(0, rows*cols):
    ax = plt.subplot(gs[k], projection = ccrs.Robinson())  
    # any of these projections are OK to use
    # PlateCarree() Robinson() Mercator() Orthographic()
    #ax.set_extent([0,50,0,90])
    ax.coastlines()
    ax.gridlines()

# Do this to get updated positions/dimensions   
plt.draw() 

# Get proper ratio here
# Computed from the last `ax` (being the current)
xmin, xmax = ax.get_xbound()
ymin, ymax = ax.get_ybound()
y2x_ratio = (ymax-ymin)/(xmax-xmin) * rows/cols

# Apply new h/w aspect ratio by changing h
# Also possible to change w using set_figwidth()
fig.set_figheight(wi * y2x_ratio)

gs.tight_layout(fig)
plt.show()

The output plot:

введите описание изображения здесь

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...