Search code examples
pythonmatplotlibplotcolorscartopy

Adjusting colormaps for geoplotting


I draw some geoplots using cartopy. Using Two Slope Normalisation and "RdBu_r" colormap to plot air temperature field.

color_map = 'RdBu_r'
fig = plt.figure(figsize=(16, 12))
ax = plt.axes(projection=prj)

norm = TwoSlopeNorm(vmin=np.min(data), vcenter=273.15, vmax=np.max(data))

filled_contour = ax.contourf(data['longitude'], data['latitude'], data, norm=norm, levels=15, transform=ccrs.PlateCarree(), cmap=color_map, extend='both')

isotherm_0 = ax.contour(data['longitude'], data['latitude'], data, levels=[273.15], colors='green', transform=ccrs.PlateCarree(), linewidths=0.5, linestyles='dashed')

I perform normalization in order to cover negative temperatures with blue shades, and positive temperatures with red shades. If the dataset contains a wide range of negative and positive temperatures, the geoplot seems to be fine:

nice color spread But if there are a lot of positive temperatures and a small amount of slightly negative ones, the map will not be built as expected: wrong color spread In the image you can see how slightly negative temperatures (the minimum is 272.6236 K) are painted in dark blue shade (extreme blue value for the colormap), instead of light blue as for temperatures slightly below zero.

When I draw a geoplot zoomed in to this negative temperatures area, I also get a nice image with proper spread of color shades: nice color spread Why are the colors not drawn correctly in case as of Pic 2? How can I avoid this?

UPDATE: Since there are no such natural extreme temperatures in my project, I've taken a fixed array and cast the colormap onto it:

# Compute min and max of data
min_val, max_val = np.min(data), np.max(data)

# Define the center (K or C)
center = 273.15 if min_val>150 else 0

# Define the temperature range for colormapping
levels = np.arange(-80, 81, 1) if center==0 else np.arange(193.15, 354.15, 1)

color_map = 'RdBu_r'

filled_contour = ax.contourf(data['lon'], data['lat'], data, levels=levels, transform=ccrs.PlateCarree(), cmap=color_map, extend='both')

In the result the colors are well spread, but around 0 degrees the colors are too weak (almost white): enter image description here

So I've added different options for the colormap: "Reds" for positive temperature only, "Blues" for negative ones, and "RdBu_r" for both. And here I defined the appropriate normalization:

# Determine normalization and colormap
if min_val >= center:
    norm = Normalize(vmin=center, vmax=max_val)
    color_map = 'Reds'
elif max_val < center:
    norm = Normalize(vmin=min_val, vmax=center)
    color_map = 'Blues'
else:
    norm = TwoSlopeNorm(vmin=min_val, vcenter=center, vmax=max_val)
    color_map = 'RdBu_r'

Then drawing it with norm=norm gave me this: enter image description here It's better in terms of colors, though I need to trim the colorbar. In this image, there is the blue color for the negtive temperatures, but it's not very representative: it's too strong for these temps slighly below zero.

Finally, in order to trim the colorbar, I've added the following:

filled_contour.set_clim(min_val-5, max_val+6) 

Which gave me this: enter image description here And while the colorbar hasn't been trimmed here, the colors here are spread even more naturally with light blue for negative (but not too white) and nice gradient for positive. Still, I have to trim the colorbar somehow...

The other problem arose with different colormap options, like the one for positive temperatures only. The colormap (for values range -80, ..., 80) is not correctly spread, starting with vmin=0:

norm = Normalize(vmin=center, vmax=max_val)
color_map = 'Reds'

enter image description here

I guess I'll have to define the other levels range for positive temps (like, np.arange(0, 81, 1)). Yet all this looks like a quick'n'dirty workaround to me. I wish there was some smart automatic way for the subject. This color fuss takes much more time than working with data etc, never could've thought it is so sophisticated...


Solution

  • I used a fixed array and a fixed colormap to achieve my goal.

    I fetched the "RdBu_r" colormap and cast it to [-50, +50] temperature range to get:

    cm = {
                -50: "#053061",
                -49: "#073466",
                ...
                49: "#750421",
                50: "#6c011f" }
    
    temperature_range = list(cm.keys())  #range of values for plotting
    temperature_range = temperature_range[::step]  #if _step_-degree gradation is required
    colors = list(cm.values())
    
    # if I want the colormap to fit the real given range of values 
    # instead of a general [-50, +50] range, I use the following 
    # temperature_range. It must be equal on both sides of zero 
    # since I use binary colormap, where middle part is always zero degrees: 
    lim = np.ceil(np.max(np.abs(dataset)))
    temperature_range = np.arange(-lim, lim+1, step)
    
    custom_cmap = ListedColormap(colors, name="rdbu_colormap")
    norm = BoundaryNorm(temperature_range, custom_cmap.N)
    
    filled_contour = ax.contourf(lons, lats, data, norm=norm, levels=temperature_range,
                                 transform=ccrs.PlateCarree(), cmap=custom_cmap, extend='both')
    cbar = plt.colorbar(filled_contour, ax=ax, orientation='horizontal', pad=0.05, aspect=50)