Search code examples
pythonmatplotliblabelcolorbarsubscript

plt.savefig() partially crops subscript character from colorbar label, despite using bbox_inches="tight"


I have a figure which contains a labelled colourbar below the x axis of the main plot. When I attempt to save this using plt.savefig(), the very bottom of the subscript character in the label is cropped from the saved image, like this, despite using bbox_inches="tight". However, if I simply save the figure manually in the pop-up window, the subscript character is not cropped, like this.

Although the latter image could be manually cropped, or cropped using additional lines in the code, I would be grateful for any advice on how to resolve this issue without the need for this additional work.

I have tried to add a line break to the colourbar label like so:

label="$U/U_{"+(u"\u221e")+"}$\n"

But this simply adds white space below the label; the bottom of the subscript character is still cropped.

I have also tried to add the line:

cb.set_label(label,labelpad=5)

But this simply offsets the label from the bottom of the colourbar; no additional padding is provided below the label to fully display the subscript character.

The code is below:

import numpy
import random
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.colors as mcolors
import matplotlib.colorbar as cbar
from matplotlib import cm

##########################################################
#  Centre colourmap so 0=white
class MidpointNormalize(mpl.colors.Normalize):
    def __init__(self,vmin=None,vmax=None,midpoint=None,clip=False):
        self.midpoint=midpoint
        mpl.colors.Normalize.__init__(self,vmin,vmax,clip)
    def __call__(self,value,clip=None):
        x,y=[self.vmin,self.midpoint,self.vmax],[0,0.5,1]
        return numpy.ma.masked_array(numpy.interp(value,x,y),numpy.isnan(value))
##########################################################

# Set min and max values
xymin=0
xymax=10
valmin=-5
valmax=5
val=numpy.zeros((xymax,xymax),dtype=float)

# Configure plot
fig,ax=plt.subplots()
ax.set_xlim([xymin,xymax])
ax.set_ylim([xymin,xymax])

# Configure colour bar
colours=plt.cm.RdBu(numpy.linspace(0,1,256)) 
colourmap=mcolors.LinearSegmentedColormap.from_list('colourmap',colours)
normalisecolors=mpl.colors.Normalize(vmin=valmin,vmax=valmax)
scalecolors=cm.ScalarMappable(norm=normalisecolors,cmap=colourmap)
label="$U/U_{"+(u"\u221e")+"}$"

for ix in range(xymin,xymax):
    for iy in range(xymin,xymax):
        xlow=ix*+1          # Calculate vertices of patch
        xhigh=(ix*1)+1
        ylow=iy*1
        yhigh=(iy*1)+1
        val[ix][iy]=random.randint(valmin,valmax)   # Patch value
        rgbacolor=scalecolors.to_rgba(val[ix][iy])  # Calculate RGBA colour for value
        ax.add_patch(patches.Polygon([(xlow,ylow),(xlow,yhigh),(xhigh,yhigh),(xhigh,ylow)],fill=True,facecolor=rgbacolor))  # Add value as polygon patch

cax,_=cbar.make_axes(ax,orientation="horizontal") 
cb=cbar.ColorbarBase(cax,cmap=colourmap,norm=MidpointNormalize(midpoint=0,vmin=valmin,vmax=valmax),orientation="horizontal",label=label)
plt.savefig("C:/Users/Christopher/Desktop/test.png",dpi=1200,bbox_inches="tight")
plt.clf
plt.close()

Solution

  • I'm afraid I don't really have a good answer for you. This appears to be related to this bug https://github.com/matplotlib/matplotlib/issues/15313

    The good news is that it is being worked on, the bad news is that there is no fix as of yet.

    Two points to consider anyway (based on reading the thread on github):

    • the higher the dpi, the worst it is. So you may want to save at a lower dpi (300 works fine for me)
    • the problem is not present on the pdf backend, so you could save your plot in pdf (and eventually convert to png if needed)

    BTW (this is unrelated to the bug in question): I'm confused by the complexity of your code. It seems to me the following code produces the same output:

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.colors import TwoSlopeNorm
    
    N=10
    valmin=-5
    valmax=5
    valmid=0
    val=np.random.randint(low=valmin, high=valmax, size=(N,N))
    cmap = 'RdBu'
    norm = TwoSlopeNorm(vcenter=valmid, vmin=valmin, vmax=valmax)
    label="$U/U_{"+(u"\u221e")+"}$"
    
    # Configure plot
    fig, ax=plt.subplots()
    im = ax.imshow(val, cmap=cmap, norm=norm, aspect='auto', origin='lower')
    cbar = fig.colorbar(im, orientation='horizontal', label=label)
    fig.savefig('./test-1200.png',dpi=1200,bbox_inches="tight")  # subscript is cut
    fig.savefig('./test-300.png',dpi=300,bbox_inches="tight")  # subscript is not cut
    fig.savefig('./test-pdf.pdf',dpi=1200,bbox_inches="tight")  # subscript is not cut
    

    enter image description here

    1200 dpi:

    enter image description here

    300 dpi:

    enter image description here

    pdf:

    enter image description here