pythonmatplotlibsubplotmatplotlib-gridspec

How to create a twin axes with gridspec


I am using two GridSpec objects to generate the following plot: enter image description here

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.figure import figaspect

plt.rcParams.update({'figure.figsize' : (10,10)})
plt.rcParams.update({'font.size' : 18})

plt.rcParams.update({'mathtext.fontset': 'stix'})
plt.rcParams.update({'font.family': 'STIXGeneral'})

My code:

n_rows = 3
n_cols = 3

gs_right = plt.GridSpec(n_rows,n_cols, left=0.25,right=0.95,hspace=0)
gs_left = plt.GridSpec(n_rows,n_cols,left=0,right=0.6,wspace=0, hspace=0)

w, h = figaspect(2.4/3)
fig = plt.figure(figsize=(w,h),constrained_layout=False)

#Generate all the subplots
rightaxs = [fig.add_subplot(gs_right[i,1]) for i in range(3)]
leftaxs = [fig.add_subplot(gs_left[i,j]) for i in range(3) for j in range(2)]

#Colorbar params
width_cbar = 0.015
bottom_cbar = 0.01
leftax_xshift = 0.001
cmap = "Reds"

for ax in leftaxs:
    if ax.is_first_row():
        if ax.is_first_col():
            ax.set_title("Model 1")
        else:
            ax.set_title("Model 2")
    if ax.is_first_col():
        vmin = 0
        vmax = 10
        im = ax.hist2d(np.arange(-3,3,0.5),np.arange(-3,3,0.5), range=[[-15,15],[-15,15]], vmin=vmin,vmax=vmax,cmap=cmap)
    else:
        im = ax.hist2d(np.arange(-15,15,1),np.arange(-15,15,1), range=[[-15,15],[-15,15]], vmin=vmin,vmax=vmax,cmap=cmap)
        cax = fig.add_axes([ax.get_position().x1+leftax_xshift,ax.get_position().y0,width_cbar,ax.get_position().height])
        cax.tick_params(direction="in", length=6)
        fig.colorbar(im[3], cax=cax, extend='both', label=r'$N$')
    
for ax in rightaxs:
    if ax.is_first_row():
        ax.set_title("Model 3")
    vmin = 0
    vmax = 2
    im = ax.hist2d(np.arange(-13,13,1),np.arange(13,-13,-1),range=[[-15,15],[-15,15]],vmin=vmin,vmax=vmax,cmap=cmap)
    cax = fig.add_axes([ax.get_position().x1,ax.get_position().y0,width_cbar,ax.get_position().height])
    cax.tick_params(direction="in", length=6)
    fig.colorbar(im[3], cax=cax, extend='both', label=r'$\Delta N$')
    ax.set_aspect('equal')

#Axes ticks and labels----------------------------------------------------------------------
lb_ticks = np.linspace(-12,12,5)
tick_length = 6

for ax in leftaxs:
    ax.set_xticks(lb_ticks)
    ax.set_yticks(lb_ticks)
    ax.tick_params(direction="in", length=tick_length)
    second_axy = ax.twinx()
    second_axy.set_ylim([-15,15])
    second_axy.set_yticks(lb_ticks)
    second_axy.set_yticklabels([])
    second_axy.tick_params(direction='in', length=tick_length)
    second_axx = ax.twiny()
    second_axx.set_xlim([-15,15])
    second_axx.set_xticks(lb_ticks)
    second_axx.set_xticklabels([])
    second_axx.tick_params(direction='in', length=tick_length)
    
    if not ax.is_last_row():
        ax.set_xticklabels([])
    else:
        ax.set_xlabel(r"$\ell$ [°]")
        
    if not ax.is_first_col():
        ax.set_yticklabels([])
    else:
        ax.set_ylabel(r"$b$ [°]")    

for ax in rightaxs:
    ax.set_xticks(lb_ticks)
    ax.set_yticks(lb_ticks)
    ax.tick_params(direction='in',length=tick_length)
    second_axy = ax.twinx()
    second_axy.set_ylim([-15,15])
    second_axy.set_yticks(lb_ticks)
    second_axy.set_yticklabels([])
    second_axy.tick_params(direction='in', length=tick_length)
    #second_axx = ax.twiny()
    #second_axx.set_xlim([-15,15])
    #second_axx.set_xticks(lb_ticks)
    #second_axx.set_xticklabels([])
    #second_axx.tick_params(direction='in', length=tick_length)
    ax.set_yticklabels([])
    
    if not ax.is_last_row():
        ax.set_xticklabels([])
    else:
        ax.set_xlabel(r"$\ell$ [°]")

plt.show()

I am having an issue with the following lines (commented-out) near the end:

second_axx = ax.twiny()
second_axx.set_xlim([-15,15])
second_axx.set_xticks(lb_ticks)
second_axx.set_xticklabels([])
second_axx.tick_params(direction='in', length=tick_length)

Just like in the subplots on the left block, I would like the above lines to produce ticks at the top of the right-side subplots. However, uncommenting the lines instead results in the following error which I don't understand:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
~\anaconda3\lib\site-packages\IPython\core\formatters.py in __call__(self, obj)
    339                 pass
    340             else:
--> 341                 return printer(obj)
    342             # Finally look for special method names
    343             method = get_real_method(obj, self.print_method)

~\anaconda3\lib\site-packages\IPython\core\pylabtools.py in <lambda>(fig)
    246 
    247     if 'png' in formats:
--> 248         png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs))
    249     if 'retina' in formats or 'png2x' in formats:
    250         png_formatter.for_type(Figure, lambda fig: retina_figure(fig, **kwargs))

~\anaconda3\lib\site-packages\IPython\core\pylabtools.py in print_figure(fig, fmt, bbox_inches, **kwargs)
    130         FigureCanvasBase(fig)
    131 
--> 132     fig.canvas.print_figure(bytes_io, **kw)
    133     data = bytes_io.getvalue()
    134     if fmt == 'svg':

~\anaconda3\lib\site-packages\matplotlib\backend_bases.py in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2191                            else suppress())
   2192                     with ctx:
-> 2193                         self.figure.draw(renderer)
   2194 
   2195                     bbox_inches = self.figure.get_tightbbox(

~\anaconda3\lib\site-packages\matplotlib\artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     39                 renderer.start_filter()
     40 
---> 41             return draw(artist, renderer, *args, **kwargs)
     42         finally:
     43             if artist.get_agg_filter() is not None:

~\anaconda3\lib\site-packages\matplotlib\figure.py in draw(self, renderer)
   1838                 ax.apply_aspect(pos)
   1839             else:
-> 1840                 ax.apply_aspect()
   1841 
   1842             for child in ax.get_children():

~\anaconda3\lib\site-packages\matplotlib\axes\_base.py in apply_aspect(self, position)
   1659         # Not sure whether we need this check:
   1660         if shared_x and shared_y:
-> 1661             raise RuntimeError("adjustable='datalim' is not allowed when both "
   1662                                "axes are shared")
   1663 

RuntimeError: adjustable='datalim' is not allowed when both axes are shared

Any advice? I would also appreciate any comments on how to improve the robustness of the code. I feel like it's a bit hard-coded because of the manually-set:


Solution

  • I realised I can completely avoid generating twin axes to set ticks at the top and on the right, by just doing:

    plt.rcParams['xtick.top'] = True
    plt.rcParams['ytick.right'] = True
    

    A big chunk of my code also gets simplified by just establishing the settings

    plt.rcParams['xtick.direction'] = 'in'
    plt.rcParams['ytick.direction'] = 'in'
    plt.rcParams['xtick.major.size'] = 6
    plt.rcParams['ytick.major.size'] = 6