matplotlibcartopy

How to add an additional colorbar block to a matplotlib contourf colorbar


I am plotting a contourf of some data with matplotlib, xarray, and cartopy. The data contouring is plotted using an ax.contourf instance, generating the colorbar in the image below. I then mask the array and hatch over all values that equal zero.

enter image description here

I would like to add an additional bar to the left side of the colorbar that is hatched out, like the following I constructed in GIMP

enter image description here

I imagine that this would be performed prior to the fig.colorbar() call, operating on the original contourf handle, but can't quite figure out how to do this.

Appreciate any advice.

Minimal sample follows, producing image below:

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy as np
    
fig = plt.figure()
ax = plt.axes(projection = ccrs.Mollweide(central_longitude=96))
ax.coastlines()

X = np.arange(-180,180,1)
Y = np.arange(-90,91,1)
X_grid, Y_grid = np.meshgrid(X,Y)
Z = np.random.random_sample((len(Y),len(X))).round(1)
Z.sort()

levels = [0,0.05,0.25,0.5,0.75,0.85,1]
CS = ax.contourf(X_grid,Y_grid,Z,transform = ccrs.PlateCarree(), cmap='plasma', transform_first=True, levels=levels)
HS = ax.contourf(X_grid,Y_grid,np.ma.masked_not_equal(Z,0),transform=ccrs.PlateCarree(),colors='none', levels=[-0.01,0.01],hatches=['X','X'], transform_first=True)

cbar = fig.colorbar(CS, orientation='horizontal', aspect=40, location='top')

enter image description here Edit: One solution, which I don't like because it isn't robust, is to define the hatched rectangle independently of the colorbar, shrink the colorbar, define the rectangle patch anchorpoint and size explicitly, and then add the rectangle to the figure. I've done that in the following code snippet, producing the image that follows (ignore that I've also made changes to the proportionality of the colorbar or the tick positions relative to the colorbar):

import matplotlib.patches as mpatches
hrect = mpatches.Rectangle((.0.9185, 0.8998), 0.057, 0.0395, ec='k',hatch='XXX', transform=fig.transFigure, figure=fig, fill=False)
fig.patches.extend([hrect])

enter image description here

This isn't a satisfying answer as the rectangle position will vary relative to the colorbar position if the figure size is changed, if the geoaxes change, etc. Ideally, this rectangle would be stuck on to the end of the colorbar object, but it doesn't seem like that's possible. One way to at least keep everything in line when changes are made is to define a specific colorbar axis with gridspec and then plot the rectangle in a separate gridspec subplot that's contiguous with the cax. Again, not elegant.

Final edit: And this is the result from the solution @JohanC provided: enter image description here


Solution

  • The approach below adds an extra level to create a box for the hatched region.

    A list of colors is created from the originally used colors, extended with a color for the first region. In the example code, white is used to color it, but you could e.g. also use the same color as the first region.

    The list of colors is used to create a ListedColormap (without a colormap, there is no information for a colormap).

    After both the contour maps and the colorbar are created, a transparent rectangle with the same hatch pattern is drawn on top of the first colorbar region.

    import matplotlib.pyplot as plt
    from matplotlib.colors import ListedColormap
    import cartopy.crs as ccrs
    import numpy as np
    
    X = np.arange(-180, 180, 1)
    Y = np.arange(-90, 91, 1)
    X_grid, Y_grid = np.meshgrid(X, Y)
    Z = np.random.random_sample((len(Y), len(X))).round(1)
    Z.sort()
    
    fig = plt.figure()
    ax = plt.axes(projection=ccrs.Mollweide(central_longitude=96))
    ax.coastlines()
    
    hatch_level = 0.01
    levels = [0, hatch_level, 0.05, 0.25, 0.5, 0.75, 0.85, 1]
    colors = ['white'] + list(plt.get_cmap('plasma', len(levels) - 2).colors)
    cmap = ListedColormap(colors)
    CS = ax.contourf(X_grid, Y_grid, Z, transform=ccrs.PlateCarree(), cmap=cmap, transform_first=True, levels=levels)
    HS = ax.contourf(X_grid, Y_grid, np.ma.masked_not_equal(Z, 0), transform=ccrs.PlateCarree(), colors='none',
                     levels=[-hatch_level, hatch_level], hatches=['XX'], transform_first=True)
    HS.set_edgecolor('dodgerblue')  # will be used for hatch marks and the contour edges
    HS.set_linewidth(0)  # don't show the contour edges
    
    cbar = fig.colorbar(CS, orientation='horizontal', aspect=40, location='top')
    
    # create a rectangle on top of the first colored region
    rect = plt.Rectangle((0, 0), hatch_level, 1,  # x between 0 and hatch_level, y between 0 and 1
                         facecolor='none',  # make the facecolor transparent
                         hatch=HS.hatches[0],  # use the same hatch pattern as in HS
                         edgecolor=HS.get_edgecolor(),  # use the same hatch color as in HS
                         lw=0)  # don't show an edge around the rectanble
    # add the rectangle to the colorbar
    cbar.ax.add_patch(rect)
    
    # get the xtick labels of the colorbar
    xtick_labels = [t.get_text() for t in cbar.ax.get_xticklabels()]
    # replace the second label with the first, and empty the first
    xtick_labels[1] = xtick_labels[0]
    xtick_labels[0] = ''
    cbar.ax.set_xticklabels(xtick_labels)
    
    plt.show()
    

    colorbar for contour plot with an extra hatched region

    Here is how it could look like if the hatched region gets the same face color as the first region:

    # ...
    colors =  list(plt.get_cmap('plasma', len(levels) - 2).colors)
    colors = [colors[0] ]+ colors
    # ...
    HS.set_edgecolor('pink')
    

    same color for hatching as for first region