Search code examples
matplotliblegendsubplotcolorbar

Matplotlib subplots how to align colorbars with other legends, or how to justify subplots to left


How can I add a colorbar scale to the 2nd & 3rd subplots, such that it is inline with my legends in the 1st and 4th subplots? Or, another way to say the question: how can I add a colorbar scale without changing the alignment/justification of the 2nd & 3rd subplots?

There are good examples available on setting colorbar locations (e.g., here on stackoverflow and in the matplotlib docs), but I still haven't been able to solve this.

Below is a reproducible example. The real data are more complicated, and this is part of a loop to produce many figures, so the "extra" stuff about setting axis limits and subplot aspect ratios is needed and will change with different datasets.

Using Python 3.8.

Reproducible example without colorbar

## Specify axes limits, tick intervals, and aspect ratio
xl, yl, xytick, ar = [-40000,120000], [-30000,10000], 20000, 0.8

## Global plot layout stuff
fig = plt.figure(figsize=(10, 7.5), constrained_layout=True)
gs = fig.add_gridspec(4, 1)
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[1, 0], sharex = ax1, sharey = ax1)
ax3 = fig.add_subplot(gs[2, 0], sharex = ax1)
ax4 = fig.add_subplot(gs[3, 0], sharex = ax1, sharey = ax3)
fig.execute_constrained_layout()
fig.suptitle('Suptitle')


## First Plot
ax1.plot([-30000, 500], [-2000, -21000], c='red', label='A')
ax1.plot([80000, 110000], [-9000, 800], c='blue', label='B')
ax1.set_title('ax1', style='italic');
ax1.set_xlabel('x');
ax1.set_ylabel('beta');
ax1.set_xlim(xl)
ax1.set_ylim(yl)
ax1.xaxis.set_major_locator(ticker.MultipleLocator(xytick))
ax1.yaxis.set_major_locator(ticker.MultipleLocator(xytick))
ax1.legend(handles=leg, bbox_to_anchor=(1.05, 1), loc='upper left')
ax1.set_aspect(aspect=ar)


## Dummy data for plots 2/3/4
x = [-15000, -2000, 0, 5000, 6000, 11000, 18000, 21000, 25000, 36000, 62000]
beta = [1000, 200, -800, 100, 1000, -2000, -5000, -5000, -15000, -21000, -1500]
y = [0.01, 0.2, 1.3, 0.35, 0.88, 2.2, 2.5, 1.25, 3.4, 4.1, 2.1]


## Second Plot
vals = ax2.scatter(x, beta, c=y, norm=mcolors.LogNorm(), cmap='rainbow')
ax2.set_title('ax2', style='italic');
ax2.set_xlabel('x');
ax2.set_ylabel('beta');
ax2.set_aspect(aspect=ar)

## Attempt to add colorbar
#cbar = fig.colorbar(vals, ax=ax2, format = '%1.2g', location='right', aspect=25)
#cbar.ax.set_ylabel('y')
#cbar.ax.yaxis.set_label_position('left')
#cbar_range = [min(y), max(y)]
#ticklabels = cbar.ax.get_ymajorticklabels()
#cbarticks = list(cbar.get_ticks())
#cbar.set_ticks(cbar_range + cbarticks)


## Third Plot
ax3.scatter(x, y, c=y, norm=mcolors.LogNorm(), cmap='rainbow')
ax3.set_title('ax3', style='italic');
ax3.set_xlabel('x');
ax3.set_ylabel('y');
ax3.yaxis.set_major_formatter(FormatStrFormatter('%1.2g'))


## Fourth Plot
ax4.scatter(x, y, c='black', label='Dots')
ax4.set_title('ax4', style='italic');
ax4.set_xlabel('x');
ax4.set_ylabel('y');
ax4.legend(bbox_to_anchor=(1.05, 1), loc='upper left')


## Clean-up, set aspect ratios
figW, figH = ax1.get_figure().get_size_inches()
_, _, w, h = ax1.get_position().bounds
disp_ratio = (figH * h) / (figW * w)
data_ratio = sub(*ax3.get_ylim()) / sub(*ax3.get_xlim())
ax3.set_aspect(aspect=disp_ratio / data_ratio )
ax4.set_aspect(aspect=disp_ratio / data_ratio)

## Clean-up, turn axis ticks back on after messing with cbar
#ax1.tick_params(axis='both', which='both', labelbottom='on')
#ax2.tick_params(axis='both', which='both', labelbottom='on')
#ax3.tick_params(axis='both', which='both', labelbottom='on')

enter image description here

Result when trying colorbar, note misalignment of second plot

enter image description here


Solution

  • Using inset_axes() solves this, as suggested in the other answer, but the parameters relative to the transform were not explained in the example, but I was able to figure it out with some research.

    The parameters in inset_axes are [x-corner, y-corner, width, height] and the transform is like a local reference. So, using [1,0,0.5,0.75] means: x = 100% or end of parent ax; y = 0% or bottom of parent ax; width = 50% of parent ax; and height = 75% of parent ax.

    Here I wanted the colorbar to be the same height as the parent ax (ax2 and ax3), very thin, and offset a little bit to be more in line with the other legends. Using cbax = ax2.inset_axes([1.1, 0, 0.03, 1], transform=ax2.transAxes) achieves this.

    This code works for any aspect ratio ar.

    import matplotlib.pyplot as plt
    import matplotlib.gridspec as gridspec
    import matplotlib.colors as mcolors
    from operator import sub
    %matplotlib inline 
    plt.style.use('seaborn-whitegrid')
    
    ## Specify axes limits, tick intervals, and aspect ratio
    xl, yl, ar = [-40000,120000], [-30000,10000], .5
    
    ## Global plot layout stuff
    fig = plt.figure(figsize=(10, 7.5), constrained_layout=True)
    gs = fig.add_gridspec(4, 1)
    ax1 = fig.add_subplot(gs[0, 0])
    ax2 = fig.add_subplot(gs[1, 0], sharex = ax1, sharey = ax1)
    ax3 = fig.add_subplot(gs[2, 0], sharex = ax1)
    ax4 = fig.add_subplot(gs[3, 0], sharex = ax1, sharey = ax3)
    fig.execute_constrained_layout()
    fig.suptitle('Suptitle')
    
    
    ## First Plot
    ax1.plot([-30000, 500], [-2000, -21000], c='red', label='A')
    ax1.plot([80000, 110000], [-9000, 800], c='blue', label='B')
    ax1.set_title('ax1', style='italic');
    ax1.set_xlim(xl)
    ax1.set_ylim(yl)
    ax1.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
    ax1.set_aspect(aspect=ar)
    
    ## Dummy data for plots 2/3/4
    x = [-15000, -2000, 0, 5000, 6000, 11000, 18000, 21000, 25000, 36000, 62000]
    beta = [1000, 200, -800, 100, 1000, -2000, -5000, -5000, -15000, -21000, -1500]
    y = [0.01, 0.2, 1.3, 0.35, 0.88, 2.2, 2.5, 1.25, 3.4, 4.1, 2.1]
    
    ## Second Plot
    vals = ax2.scatter(x, beta, c=y, norm=mcolors.LogNorm(), cmap='rainbow')
    ax2.set_title('ax2', style='italic');
    ax2.set_aspect(aspect=ar)
    cbax = ax2.inset_axes([1.1, 0, 0.03, 1], transform=ax2.transAxes)
    cbar2 = fig.colorbar(vals, cax=cbax, format = '%1.2g', orientation='vertical')
    
    ## Third Plot
    ax3.scatter(x, y, c=y, norm=mcolors.LogNorm(), cmap='rainbow')
    ax3.set_title('ax3', style='italic');
    cbax = ax3.inset_axes([1.1, 0, 0.03, 1], transform=ax3.transAxes)
    cbar3 = fig.colorbar(vals, cax=cbax, format = '%1.2g', orientation='vertical')
    
    ## Fourth Plot
    ax4.scatter(x, y, c='black', label='Dots')
    ax4.set_title('ax4', style='italic');
    ax4.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
    
    ## Clean-up, set aspect ratios
    figW, figH = ax1.get_figure().get_size_inches()
    _, _, w, h = ax1.get_position().bounds
    disp_ratio = (figH * h) / (figW * w)
    data_ratio = sub(*ax3.get_ylim()) / sub(*ax3.get_xlim())
    ax3.set_aspect(aspect=disp_ratio / data_ratio )
    ax4.set_aspect(aspect=disp_ratio / data_ratio)
    
    ## Colorbars
    cbar2.ax.set_ylabel('y')
    cbar2.ax.yaxis.set_label_position('left')
    cbar3.ax.set_ylabel('y')
    cbar3.ax.yaxis.set_label_position('left')
    

    Result with aspect ratio = 0.5 for top 2 plots enter image description here

    Result with aspect ratio = 2 for top 2 plots

    enter image description here