Gridspec and top alignment of two subplots of different height

Hi Matplotlib Users,

I could not find an answer to this on stackoverflow.

I have two subplots in a figure, nrow=1, ncols=2.
They are of different height. The left is bigger than the right one. Matplotlib aligns them not at the top, but centered.
ax1.set_anchor(‘N’) shows no effect.
Edit: I think this is when I provide in ax1.imshow the aspect explicity.

The idea is here to have on the left the whole image, on the right a zoomed in.
I don’t know what is better: To select the indices or set the zoom via the limits?

The last question is about the height ratio in gridspec. Only height_ratios=[1] works, not 0.5,0.5.
But why can I not set the height ratio for both pictures equal? 0.5., 0.5 because 0.5+0.5=1=nrows?

import numpy as np
import matplotlib as mpl

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec


# Data for plotting
Z = np.arange(10000).reshape((100, 100))
Z[:, 50:] = 1

index_array=[40,60,40,50]

figure_size=(10, 7)
fig = plt.figure(figsize=figure_size)
gs = gridspec.GridSpec(nrows=1, ncols=2, height_ratios=[1], width_ratios=[1.2,0.8],wspace=0.5)
fig.subplots_adjust(left=0.05, bottom=0.06, right=0.95, top=0.94, wspace=0.2)         
    
ax0 = fig.add_subplot(gs[0, 0])


im0=ax0.imshow(Z, aspect='auto')


ax1 = fig.add_subplot(gs[0, 1])

im1=ax1.imshow(Z, aspect= (index_array[1]-index_array[0])/(index_array[3]-index_array[2]))
ax1.set_xlim([40,60])
ax1.set_ylim([40,50])

axes=[ax0,ax1]

#common colorbar
plt.colorbar(im0,  ax=axes, orientation='vertical')
      

This may be tricky to fix. I think the issue is that when you specify a fixed aspect ratio on an axes (which says moving a given distance on the screen in one direction in data space moves a fixed distance in the other direction) we have three inputs from the user that we can not respect all of:

  1. The ratio of x-distance to y-distance on the screen matching the x-data to y-data ratio matching the fixed aspect ratio
  2. the data limits of your the axes
  3. the dimension on the screen of the axes

At draw time we attempt to adjust either the data limits or the axes shape / location to ensure that the ratio is correct (which you can control via ax.set_aspect(..., adjustable='datalim') (see
matplotlib.axes.Axes.set_aspect — Matplotlib 3.5.1 documentation ) or ax.set_adjustable (see matplotlib.axes.Axes.set_adjustable — Matplotlib 3.5.1 documentation) .

The source of the conflict is that grid spec and the aspect adjuster are not aware of each other so will stomp on each other’s toes.

2 Likes

Thank you for your explanation. I circumvent the issue a bit buy simply plotting both graphs on top of each other.
Sorry about this, but thanks to you I got aware of a new keyword adjustable.
I think I understood point 1 and 2, but not 3.

Could I ask something else in this context, please?
When the upper image gets a colorbar and the lower image below not, it seems that the total width of the upper plot is the actual subplot width plus width of colorbar. Is this correct?
When I try

for axi in axes:
            axi.set_anchor('C')

Then the lower plot is slightly misadjusted because it does not have its own colorbar.

Mini example:

import numpy as np
import matplotlib as mpl

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec


index_array=[40,60,40,50]


def plot_scan(index_array):
    
        # Data for plotting
        Z = np.arange(10000).reshape((100, 100))
        Z[:, 50:] = 1
        
        #figure_size=(8, 5)
        figure_size=(5, 9)
        fig = plt.figure(figsize=figure_size)
    
        gs = gridspec.GridSpec(nrows=2, ncols=1 ,wspace=0, hspace=0.15, width_ratios=[1], height_ratios=[1.25,0.75]) #height_ratios=[1], width_ratios=[1.2,0.8]
        fig.subplots_adjust(left=0.1, bottom=0.06, right=0.85, top=0.94, hspace=0) 
        
        ax0 = fig.add_subplot(gs[0, 0])
        vmax = np.percentile(Z, 90)
        
        im0=ax0.imshow(Z, vmax=vmax, aspect='auto') #norm=mpl.colors.LogNorm(vmin=0.001, vmax=1)

        
        ax1 = fig.add_subplot(gs[1, 0])
        im1=ax1.imshow(Z,vmax=vmax, aspect= (index_array[1]-index_array[0])/(index_array[3]-index_array[2]))
        
        ax1.set_xlim([index_array[0],index_array[1]])
        ax1.set_ylim([index_array[3],index_array[2]])
        
        #ax0.set_anchor('N')
        #ax1.set_anchor('S')
        
        axes=[ax0,ax1]
        for axi in axes:
            axi.set_anchor('C')

        #colorbar
        plt.colorbar(im0,  ax=ax0, orientation='vertical', pad=0.02)
        
        plt.show()
    
plot_scan(index_array)

At the end of the day the Axes has some location (canonically in Figure Fraction) where it is in the Figure.

Gridspec and color bar are a nicer to use layer to manage the location of the Axes (grid spec puts things in a grid (as the name suggests) and colorbar will “steal space” from it’s host), but at draw time we look at ax.get_position() and that is where the Axes goes in the Figure (the lowest level way to add an Axes to a Figure is fig.add_axes that takes in a rectangle and makes an Axes there. I am 95% sure that all of the other methods eventually fall back to using this core method). Note that the Axes is located in figure fraction so if you change the size / shape of your Figure the Axes adjust to fill the space the same way (which because the decorations tend to be text which have a fixed absolute size does not always do what you want which leads us to constrained layout (and tight layout before it) to adjust the Axes to use as much space as possible at the current Figure size, but I digress).

Thus, if the the x range in [0, 1], the y range is [100, 200], your figure is 4in by 5in, your Axes is placed at [.1, .9, .1, .9], and you want a data aspect ratio of 5 we are over constrained! If the user sets a fixed aspect ratio we have to adjust (and from the point of the view of the library discard user input/intention) either the data limits or the position+shape of the Axes and the adjustable parameter is how what we use guide which way we go.

@Tacaswell Tahnk you. Puh, it is hard for me to understand in full detail. I am not that deep into matplotlib.
One thing I notice this night, was that with subplots it seems adjusting the width of an axis via ax.set_position had no effect.
I also saw a hack where somebody aligned two subplots on top of each other, but the top colorbar was on axis 1 instead of 0 and the second colorbar on fourth axis was remove. I could not reproduce this because I don’t know exactly how to switch between gridspec and axes, but it was a nice hack.

Thus, if the the x range in [0, 1], the y range is [100, 200], your figure is 4in by 5in, your Axes is placed at [.1, .9, .1, .9], and you want a data aspect ratio of 5 we are over constrained! If the user sets a fixed aspect ratio we have to adjust (and from the point of the view of the library discard user input/intention) either the data limits or the position+shape of the Axes and the adjustable parameter is how what we use guide which way we go.
How do you end up with 4in by 5in based on x and y range? Is it in connection with dpi?
Okay, I did not know that user input is discarded. I imagined it that within the figure box/frame, I could freely places plots and adjust them.

For the moment I am happy with the result. I just find it a bit odd that a colorbar steels space from the axis. Would it not be nice to have access exactly to the width of a subplot without the colorbar?
And without going more into detail, I don’t understand what determines the distance of the colorbar to the plot. I played around with the pad argument, but even when I put it to 100 or -100 the colorbar did not move position.
Sorry, if I am doing something wrong.
Thanks for the exchange.

The Figure object knows how big it is (fig.set_size_inches() and fig.get_size_inches()) in physical units and then knows its DPI to get to pixels.

I remember this was the standard size for each figure imho.
Can I get the width of a subplot without the colorbar width, or do they form one unit and the width of a subplot with always include the colorbar, please?

When you call colorbar you can pass the cax= keyword argument that will use the given Axes for the colorbar rather than trying to steal space from the Axes that contains the ScalarMappable the colorbar will be associated with.

Using this you can get more control over exactly where the axes the colorbar is in goes.