Search code examples
pythonmatplotlibplotboxplotcolorbar

Trimming a color bar after normalize in matplotlib


I have aligned a color bar to the x-axis of a horizontal box plot.

As you can see in the figure at the bottom, there is a blank area along the x-axis.

So I want to trim the x-axis together with the color bar, which was normalized by vmin and vmax

Any idea how to accomplish that?

Thanks a lot!

test_data = [np.random.normal(mean, 1, 10) for mean in range(400, 500, 10)]

### set sns style
sns.set_style('white')

### set fig, ax
fig = plt.figure(figsize=(4,2))
ax1 = fig.add_axes([0.10,0.10,1.2,2])

### Define color bar
# choose color map
cmap = plt.get_cmap('nipy_spectral')
# normalize min and max
norm = mpl.colors.Normalize(vmin=380,vmax=780)
# assign cmap and norm
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])

### Plot color bar (change "pad" to adjust distance of color bar to x-axis)
plt.colorbar(sm, ticks=np.linspace(380,780,11), location="bottom", pad=0)

### Plot boxplot
bplot = ax1.boxplot(test_data, vert=False, patch_artist=True)

### Set axis
ax1.set_yticklabels(["a","b","c","d","e","f","g","h","i","j"])
ax1.get_xaxis().set_visible(False)
ax1.set_xlim(380,780)   

plt.show()

enter image description here


Solution

  • The easiest way to place the colorbar exactly aligned with the main ax, is to also create its ax as cax = fig.add_axes([...]) using the same x and width as the main ax and adapting the y and height. Then the colorbar can be created as plt.colorbar(sm, ticks=np.linspace(380, 780, 11), orientation='horizontal', cax=cax).

    As you want to reduce part of the colorbar, it might be easier to draw it via imshow() using an extent to align it with the x-axis.

    import matplotlib.pyplot as plt
    import seaborn as sns
    import numpy as np
    import matplotlib as mpl
    
    test_data = [np.random.normal(mean, 1, 10) for mean in range(400, 500, 10)]
    
    sns.set_style('white')
    
    fig = plt.figure(figsize=(10, 3))
    ax1 = fig.add_axes([0.10, 0.17, 0.88, 0.80])  # x0, y0, width, height in figure coordinates
    cax = fig.add_axes([0.10, 0.12, 0.88, 0.05], sharex=ax1)  # use same x0 and width
    
    ### Define color bar
    cmap = plt.get_cmap('nipy_spectral')
    norm = mpl.colors.Normalize(vmin=380, vmax=780)
    xmin, xmax = 380, 500
    cax.imshow(np.linspace(xmin, xmax, 100).reshape(1, -1), extent=[xmin, xmax, 0, 1], cmap=cmap, norm=norm, aspect='auto')
    cax.set_yticks([])
    
    bplot = ax1.boxplot(test_data, vert=False, patch_artist=True)
    
    ax1.set_yticklabels(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"])
    ax1.set_xlim(xmin, xmax)
    ax1.tick_params(axis='x', labelbottom=False)
    cax.set_xticks(np.arange(xmin, xmax + 1, 20))
    
    plt.show()
    

    plot with attached colorbar

    imshow() can also be used to color the boxplot rectangles in a similar way. The example below uses adapted example data to better illustrate the concept. Note that imshow resets the xlims and ylims, so they need to be saved first and reset afterwards.

    test_data = [np.random.uniform(mean - 20, mean + 20, 10) for mean in range(400, 500, 10)]
    
    sns.set_style('white')
    
    fig = plt.figure(figsize=(10, 3))
    ax1 = fig.add_axes([0.10, 0.17, 0.88, 0.80])  # x0, y0, width, height in figure coordinates
    cax = fig.add_axes([0.10, 0.12, 0.88, 0.05], sharex=ax1)  # use same x0 and width
    
    ### Define color bar
    cmap = plt.get_cmap('nipy_spectral')
    norm = mpl.colors.Normalize(vmin=380, vmax=780)
    xmin, xmax = 380, 510
    cax.imshow(np.linspace(xmin, xmax, 200).reshape(1, -1), extent=[xmin, xmax, 0, 1], cmap=cmap, norm=norm, aspect='auto')
    cax.set_yticks([])
    
    bplot = ax1.boxplot(test_data, vert=False, patch_artist=True, medianprops={'color': 'white'})
    
    ax1.set_yticklabels(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"])
    ax1.tick_params(axis='x', labelbottom=False)
    cax.set_xticks(np.arange(xmin, xmax + 1, 20))
    ymin, ymax = ax1.get_ylim()
    
    for art in ax1.artists:
        art.set_facecolor('none')  # make transparent
        x0, y0, x1, y1 = art.get_path().get_extents().extents
        ax1.imshow(np.linspace(x0, x1, 100).reshape(1, -1), extent=[x0, x1, y0, y1],
                   cmap=cmap, norm=norm, aspect='auto', zorder=0)
    ax1.set_xlim(xmin, xmax)
    ax1.set_ylim(ymin, ymax)
    

    boxplot with gradient fill