Search code examples
pythonmatplotlibcolorbarcartopy

Issues with creating a custom colorbar


I am trying to create a custom colorbar with discrete intervals using this resource (https://matplotlib.org/3.1.1/tutorials/colors/colorbar_only.html), but I am running into this error which references my 'cb2' line in my code:

Error: "AttributeError: 'GeoContourSet' object has no attribute 'set'"

import xarray as xr
from sklearn.linear_model import LinearRegression
import proplot as pplt
import cartopy as ct
import matplotlib as mpl
import matplotlib.pyplot as plt
import colormaps as cmaps
import matplotlib.colors as colors

fig, axs = pplt.subplots(ncols = 2, nrows = 1, axwidth = 7, proj='pcarree')
ax1, ax2 = axs


a = ax1.contourf(era_cape['lon'], era_cape['lat'], regression,  add_colorbar = False, extend = 'both')

axs.format(coast=True, latlim = (20,51), lonlim = (234,293), innerborders = True)
axs.add_feature(ct.feature.OCEAN, zorder=100, edgecolor='k', color = 'white')
axs.add_feature(ct.feature.COASTLINE, zorder=100, edgecolor='k')

cmap1 = mpl.colors.ListedColormap(['purple','navy','slateblue','blue','skyblue','lightblue','aliceblue','yellow','gold','orange','orangered','red','firebrick','darksalmon'])
bounds = [-600, -480, -320, -160, -80, -40, -20, 0, 20, 40, 80, 160, 320, 480, 600]
norm = mpl.colors.BoundaryNorm(bounds, cmap1.N, extend = 'neither')
cb2 = mpl.colorbar.ColorbarBase(a, cmap = cmap1, norm = norm, ticks = bounds, spacing = 'uniform')

In this example, I am relatively new to coding and I am not sure if there is anything I need to do to my data to get it to 'fit' into my custom colorbar.

If I I changed the cb2 line to read:

cb2 = fig.colorbar(a, cmap = cmap, norm = norm, ticks = bounds)

This gives me a colorbar but it is set to an old, cmap (RdBu) rather than the new one I have created. The ticks are visible but only go up about 1/4 way on the colorbar. If I change the line back to

cb2 = mpl.colorbar.ColorbarBase()

I get the attribute error mentioned above again. In this example, era_cape['lat'] and era_cape['lon'] is 2D data over the United States with data spanning from roughly -600 to 600.

{'coords': {'lon': {'dims': ('lon',), 'attrs': {'units': 
'degrees_east', 'short_name': 'lon', 'long_name': 'longitude'}, 
'data': [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 
2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0, 4.25, 4.5, 4.75, 5.0, 5.25, 
5.5, 5.75, 6.0, 6.25, 6.5, 6.75, 7.0, 7.25, 7.5, 7.75, 8.0, 8.25, 
8.5, 8.75, 9.0, 9.25, 9.5, 9.75]}, 'lat': {'dims': ('lat',), 
'attrs': {'qualifier': 'Gaussian', 'units': 'degrees_north', 
'short_name': 'lat', 'long_name': 'latitude'}, 'data': [90.0, 
89.75, 89.5, 89.25, 89.0, 88.75, 88.5, 88.25, 88.0, 87.75, 87.5, 
87.25, 87.0, 86.75, 86.5, 86.25, 86.0, 85.75, 85.5, 85.25, 85.0, 
84.75, 84.5, 84.25, 84.0, 83.75, 83.5, 83.25, 83.0, 82.75, 82.5, 
82.25, 82.0, 81.75, 81.5, 81.25, 81.0, 80.75, 80.5, 80.25]}}, 
'attrs': {'title': 'p_cal_daily2monthly_era5.ncl', 'program'

Solution

  • There's a lot going on in your sample code, and without data to replicate the error it requires some guessing. So perhaps you can simplify it a little and use toy data (or publicly available otherwise)?

    I suspect the error comes from the fact that you pass the result from ax.contourf to mpl.colorbar.ColorBase. The latter expects an axes object as the argument, and that should be a dedicated axes for the colorbar, not the axes of you actual plot. But instead of an axes, you provide the result of contourf (a GeoContourSet), which is not appropriate and I'm guessing triggers the error you experienced.

    Often more high-level functions are used to avoid having to create a colorbar axes (cax) explicitly (see below).

    I'm not familiar with proplot myself, so in the example below I have removed it for simplicity. I don't think that matters in the end and you could probably add it back in for the functionality that you need.

    A simplified example, focusing on the colorbar specifically, is shown below:

    import cartopy.crs as ccrs
    import cartopy.feature as cfeature
    import matplotlib as mpl
    import matplotlib.pyplot as plt
    import numpy as np
    
    # generate toy data, example data from:
    # https://matplotlib.org/stable/gallery/images_contours_and_fields/contourf_demo.html
    x = y = np.arange(-3.0, 3.01, 0.025)
    X, Y = np.meshgrid(x, y)
    Z1 = np.exp(-X**2 - Y**2)
    Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
    Z = (Z1 - Z2) * 2
    
    # scale toy example data to global
    lon = x * 180/3
    lat = y * 90/3
    regression = Z * 300
    
    # define the colormap and scaling
    cmap = mpl.colors.ListedColormap([
        'purple','navy','slateblue','blue','skyblue','lightblue', 'aliceblue',
        'yellow','gold','orange','orangered','red','firebrick','darksalmon',
    ])
    bounds = [-600, -480, -320, -160, -80, -40, -20, 0, 20, 40, 80, 160, 320, 480, 600]
    norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
    
    # create the figure and axes
    fig, ax = plt.subplots(
        1,1, figsize=(8,5), facecolor="w", layout="compressed", 
        subplot_kw=dict(projection=ccrs.PlateCarree()),
    )
    
    # plot the contours, note that "cf" is also a mappable that could 
    # be used instead of "im" below
    cf = ax.contourf(
        lon, lat, regression, cmap=cmap, norm=norm, 
        transform=ccrs.PlateCarree(),
    )
    
    # create the colorbar
    im = mpl.cm.ScalarMappable(cmap=cmap, norm=norm)
    cb = fig.colorbar(
        im, ax=ax, ticks=bounds, spacing="uniform", 
        orientation="horizontal", shrink=0.8,
    )
    
    ax.add_feature(cfeature.OCEAN, ec="k", fc="w", zorder=100)
    ax.add_feature(cfeature.COASTLINE, ec="k", zorder=100)
    

    Which results in: enter image description here

    The use of fig.colorbar allows you to pass your main axes (for plotting), and makes Matplotlib automatically create a reasonable colorbar axes for you. You can also provide that axes yourself (cax=cax), but that's mainly useful if you need more control on the specific location for example, like placing it in/over your main axes.