Annotating subplots in matplotlib scales the figure to the largest axes

Viewed 528

When I make figure with 5 subplots and annotate the bars in each subplot, matplotlib appears to scale the figure so that the maximum from the largest y-axis scales to the smallest y-axis.

I can't describe the problem too well, but see this image:

where there's tons of white-space above where the figure should begin.

However, the figure would ideally look like this

this

When I set the 4 smallest axes to have the same upper y-limit as the largest axis, then the figure scales correctly, but for the purpose of the visualization, I would prefer not to do that.

Why does this happen? Is there anyway to control the figure so that it's not automatically scaled as in the first image? Or otherwise, a more appropriate way of plotting what I hope to achieve?

The code I'm using to generate the figure:

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.patches import Patch
from matplotlib import rcParams
rcParams['font.family'] = 'sans-serif'
rcParams['font.sans-serif'] = ['Arial']
department = ["100", "1,000", "10,000", \
              "100,000", "1,000,000"]
quarter = ["Serial", "MPI", "CUDA", "Hybrid"]
budgets = np.array([[0.049979, 0.43584,  2.787366, 19.75062, 201.6935],\
                    [2.184624, 0.175213, 0.677837, 5.265575, 46.33678],\
                    [0.050294, 0.068537, 0.23739,  1.93778,  18.55734],\
                    [3.714284, 3.9917,   4.977599, 6.174967, 37.732232]])

budgets = np.transpose(budgets)
em = np.zeros((len(department), len(quarter)))

# set up barchart
x = np.arange(len(department)) # label locations
width = 0.8    # width of all the bars

# set up figure
fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(1, 5)
axes = [ax1, ax2, ax3, ax4, ax5]

# generate bars
rects = []
color = ["tomato", "royalblue", "limegreen", "orange"]
n = len(quarter)
for i in range(n):
    bar_x = x - width/2.0 + i/float(n)*width + width/(n*2)

    m = len(budgets[:,i])
    for j in range(m):
        bar_x = x[j] - width/2.0 + i/float(n)*width + width/(n*2)
        e = budgets[j,i]
        #bar_x = x - width/2.0 + i/float(n)*width + width/(n*2)
        rects.append(axes[j].bar(bar_x, e, width=width/float(n), \
                label=quarter[i], color=color[i]))

# set figure properties
fig.set_size_inches(12, 2.5)
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
nAx = len(axes)
for i in range(nAx):
    #axes[i].set_aspect("auto")
    axes[i].tick_params(axis='x', which='both', bottom=False, top=False, 
                        labelbottom=False)

ax1.set_ylabel("Time (ms)")
for i in range(nAx):
    axes[i].yaxis.grid(which="major", color="white", lw=0.75)
ax1.set_ylim([0, 4])

fig.suptitle("Time per iteration for differing dataset sizes")   # title

for i in range(nAx):
    axes[i].set_xlabel(department[i])

# annotate bars
for i in range(nAx):
    for rect in rects:
        j = 0;
        for bar in rect:
            y_bottom, y_top = axes[i].get_ylim() # axis limits

            height = bar.get_height() # bar's height

            va = 'bottom'
            offset = 3
            color = 'k'
            fg = 'w'

            # keep label within plot
            if (y_top < 1.1 * height):
                offset = -3
                va = 'top'
                color='w'
                fg = 'k'

            # annotate the bar
            axes[i].annotate('{:.2f}'.format(height),
                              xy=(bar.get_x() + bar.get_width()/2, height),
                              xytext=(0,offset),
                              textcoords="offset points",
                              ha='center', va=va, color=color)


# set custom legend
legend_elements = [Patch(facecolor='tomato', label='Serial'),
                   Patch(facecolor='royalblue', label='MPI'),
                   Patch(facecolor='limegreen', label='CUDA'),
                   Patch(facecolor='orange', label='Hybrid')]
plt.legend(handles=legend_elements, loc="upper center", fancybox=False, 
           edgecolor='k', ncol=4, bbox_to_anchor=(-2, -0.1))

plt.show()
2 Answers

This is a partial answer.

This might be a bug, since I couldn't reproduce the problem until I switched to a Jupyter notebook in a Debian system (different hardware too). Your figure gets drawn correctly in my macOS Jupyter notebook, and in Debian when displayed from a .py script.

The problem appears to be with your annotations. If you make the tight_layout call after annotation, you might get a warning like this:

<ipython-input-80-f9f592f5efc5>:88: UserWarning: Tight layout not applied. The bottom and top margins cannot be made large enough to accommodate all axes decorations. 
  fig.tight_layout(rect=[0, 0.03, 1, 0.95])

It seems like the annotate function is calculating some totally wacky coordinates for your annotations, though the text ends up in the right spot. If you remove them, the white space disappears. You can try calculating the xy coordinates a for your annotations a different way. This might get you started:

        axes[i].annotate('{:.2f}'.format(height),
                          xy=(bar.get_x() + bar.get_width()/2, height),
                          xytext=(0,offset),
                          textcoords="offset points",
                          xycoords="axes points", # change
                          ha='center', va=va, color=color)

Output:

enter image description here

To correctly calculate the points, you can try using the appropriate axis transformation, though again, I couldn't get it to work and it might be related to a bug.

try putting the fig.tight_layout(rect=[0, 0.03, 1, 0.95]) after all the plotting commands, as below.

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.patches import Patch
from matplotlib import rcParams
rcParams['font.family'] = 'sans-serif'
rcParams['font.sans-serif'] = ['Arial']
department = ["100", "1,000", "10,000", \
              "100,000", "1,000,000"]
quarter = ["Serial", "MPI", "CUDA", "Hybrid"]
budgets = np.array([[0.049979, 0.43584,  2.787366, 19.75062, 201.6935],\
                    [2.184624, 0.175213, 0.677837, 5.265575, 46.33678],\
                    [0.050294, 0.068537, 0.23739,  1.93778,  18.55734],\
                    [3.714284, 3.9917,   4.977599, 6.174967, 37.732232]])

budgets = np.transpose(budgets)
em = np.zeros((len(department), len(quarter)))

# set up barchart
x = np.arange(len(department)) # label locations
width = 0.8    # width of all the bars

# set up figure
fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(1, 5)
axes = [ax1, ax2, ax3, ax4, ax5]

# generate bars
rects = []
color = ["tomato", "royalblue", "limegreen", "orange"]
n = len(quarter)
for i in range(n):
    bar_x = x - width/2.0 + i/float(n)*width + width/(n*2)

    m = len(budgets[:,i])
    for j in range(m):
        bar_x = x[j] - width/2.0 + i/float(n)*width + width/(n*2)
        e = budgets[j,i]
        #bar_x = x - width/2.0 + i/float(n)*width + width/(n*2)
        rects.append(axes[j].bar(bar_x, e, width=width/float(n), \
                label=quarter[i], color=color[i]))

# set figure properties
fig.set_size_inches(12, 2.5)
#fig.tight_layout(rect=[0, 0.03, 1, 0.95])
nAx = len(axes)
for i in range(nAx):
    #axes[i].set_aspect("auto")
    axes[i].tick_params(axis='x', which='both', bottom=False, top=False, 
                        labelbottom=False)

ax1.set_ylabel("Time (ms)")
for i in range(nAx):
    axes[i].yaxis.grid(which="major", color="white", lw=0.75)
ax1.set_ylim([0, 4])

fig.suptitle("Time per iteration for differing dataset sizes")   # title

for i in range(nAx):
    axes[i].set_xlabel(department[i])

# annotate bars
for i in range(nAx):
    for rect in rects:
        j = 0;
        for bar in rect:
            y_bottom, y_top = axes[i].get_ylim() # axis limits

            height = bar.get_height() # bar's height

            va = 'bottom'
            offset = 3
            color = 'k'
            fg = 'w'

            # keep label within plot
            if (y_top < 1.1 * height):
                offset = -3
                va = 'top'
                color='w'
                fg = 'k'

            # annotate the bar
            axes[i].annotate('{:.2f}'.format(height),
                              xy=(bar.get_x() + bar.get_width()/2, height),
                              xytext=(0,offset),
                              textcoords="offset points",
                              ha='center', va=va, color=color)


# set custom legend
legend_elements = [Patch(facecolor='tomato', label='Serial'),
                   Patch(facecolor='royalblue', label='MPI'),
                   Patch(facecolor='limegreen', label='CUDA'),
                   Patch(facecolor='orange', label='Hybrid')]
plt.legend(handles=legend_elements, loc="upper center", fancybox=False, 
           edgecolor='k', ncol=4, bbox_to_anchor=(-2, -0.1))

fig.tight_layout(rect=[0, 0.03, 1, 0.95])

plt.show()
Related