Dear Python VIS community.
Imagine the following situation:
You ran an experiment in an earth system model (ESM) where you altered some input parameters relative to a control run of the same ESM. Now you look at the surface air temperature and since you have this information for both your experiment and your control run, you can compare the two data sets. Say you want to look at the seasonal difference in air temperature between the experiment and the control (i.e. experiment - contorl = difference). This would result in four maps, one for each season (Winter, Spring, Summer, Autumn, defined as "DJF", "MAM", "JJA", "SON").
import matplotlib.pyplot as plt import matplotlib.colors as mcol import matplotlib.cbook as cbook import matplotlib.colors as colors import numpy as np import sys winter = np.random.random((96,144)) winter[0:48,:] = winter[0:48,:] * 3 winter[48:,:] = winter[48:,:] * -6 spring = np.random.random((96,144)) spring[0:48,:] = spring[0:48,:] * 4 spring[48:,:] = spring[48:,:] * -6 summer = np.random.random((96,144)) summer[0:48,:] = summer[0:48,:] * 10 summer[48:,:] = summer[48:,:] * -2 autumn = np.random.random((96,144)) autumn[0:48,:] = autumn[0:48,:] * 4 autumn[48:,:] = autumn[48:,:] * -7 Cool data! But how do you visualize this using matplotlib? Okay you decide for filled contours from matplotlib. This will yield a map including a colorbar for each season. Now that is cool and all, but you would like to have the four maps in subplots (easy!, see Figure 1).
fig, axes = plt.subplots(2, 2) axes = axes.flatten() seasons = zip(axes, [winter, spring, summer, autumn]) for pair in seasons: im = pair[0].contourf(pair[1]) plt.colorbar(im, ax=pair[0]) Figure 1. Suplots with individual colorbar.
Since you want to look at the difference, you can tell that 0 will be important, because your temperature can either be the same (x=0), or it can be regionally cooler (x < 0) or warmer (0 < x).
In order to have the value zero (0) exactly split the colormap you decide for a diverging colormap (based on this example: https://matplotlib.org/3.2.0/gallery/userdemo/colormap_normalizations_diverging.html#sphx-glr-gallery-userdemo-colormap-normalizations-diverging-py). Perfect you think?
colors_low = plt.cm.RdBu_r(np.linspace(0, 0.3, 256)) colors_high = plt.cm.RdBu_r(np.linspace(0.5, 1, 256)) all_colors = np.vstack((colors_low, colors_high)) segmented_map = colors.LinearSegmentedColormap.from_list('RdBu_r', all_colors) divnorm = colors.TwoSlopeNorm(vmin=-7, vcenter=0, vmax=10) fig, axes = plt.subplots(2, 2) axes = axes.flatten() seasons = zip(axes, [winter, spring, summer, autumn]) for pair in seasons: im = pair[0].contourf(pair[1], cmap=segmented_map, norm=divnorm, vmin=-7, vmax=10) plt.colorbar(im, ax=pair[0]) Figure 2. Diverging colormap with 0 in as the center.
Not quite, because although you supply contourf() with the overall vmin and vmax and your derived colormap, the levels (i.e. ticks) in the colorbar are not the same for the four plots ("WHY?!?", you scream)!
Aha you find that you need to supply the same levels to contourf() in all four subplots (based on this: https://stackoverflow.com/questions/53641644/set-colorbar-range-with-contourf). But how do you exploit the functionality of contourf, that automatically choses appropriate contour levels?
You think that you could invisibly plot the four individual images, and then extract the color levels from each (im = contourf(), im.levels, https://matplotlib.org/3.3.1/api/contour_api.html#matplotlib.contour.QuadContourSet) and from this create a unique set of levels that combines the maximum and minimum from the extracted color levels.
# -1- Create the pseudo-figures for extraction of color levels: fig, axes = plt.subplots(2, 2) axes = axes.flatten() seasons = zip(axes, [winter, spring, summer, autumn]) c_levels = [] for pair in seasons: im = pair[0].contourf(pair[1], cmap=segmented_map, norm=divnorm, vmin=-7, vmax=10) c_levels.append(im.levels) # -1.1- Clear the figure plt.clf() # -2- Find the colorbars with the most levels below and above 0: lower = sys.maxsize lower_i = 0 higher = -1 * sys.maxsize higher_i = 0 for i, c_level in enumerate(c_levels): if np.min(c_level) < lower: # extract the index for the array with the minimum value lower = np.min(c_level) lower_i = i if np.max(c_level) > higher: # extract the index for the array with the maximum value higher = np.max(c_level) higher_i = i # -3- Create the custom color levels as a combination of the minimum and maximum found above custom_c_levels = [] for level in c_levels[lower_i]: if level <= 0: # define the levels for the negative section, including 0 custom_c_levels.append(level) for level in c_levels[higher_i]: if level > 0: # define the levels for the positive section, excluding 0 custom_c_levels.append(level) custom_c_levels = np.array(custom_c_levels) # -4- create the new normalization to go along with the new color levels v_min = custom_c_levels[0] v_max = custom_c_levels[-1] divnorm = divnorm = colors.TwoSlopeNorm(vmin=v_min, vcenter=0, vmax=v_max) # -5- plot the figures fig, axes = plt.subplots(2, 2) axes = axes.flatten() seasons = zip(axes, [winter, spring, summer, autumn]) for pair in seasons: im = pair[0].contourf(pair[1], levels=custom_c_levels, cmap=segmented_map, norm=divnorm, vmin=v_min, vmax=v_max) # -6- Get the positions of the lower right and lower left plot left, bottom, width, height = axes[3].get_position().bounds first_plot_left = axes[2].get_position().bounds[0] # -7- the width of the colorbar width = left - first_plot_left + width # -8- Add axes to the figure, to place the color bar in colorbar_axes = plt.axes([first_plot_left, bottom - 0.15, width, 0.03]) # -9- Add the colour bar cbar = plt.colorbar(im, colorbar_axes, orientation='horizontal') # -10- Label the colour bar and add ticks cbar.set_label("Custom Colorbar") Figure 3. Final figure, including correct contours and single colorbar.
And it works!
But I'm sure the community is able to do that in a much more straight forward kind of way.
Challenge accepted? Then I would really appreciate if you could show me how to do that in a more simple approach.
ps: I give random data because I think it is more reasonable for the exercise (not using additional libraries such as iris and cartopy). However you can imagine that we are actually looking at a world map with land surface temperature ;).

np.linspace? I'm not super familiar with making contour plots and not sure what its auto-coloring does that's more complicated than just that? \$\endgroup\$np.linspaceif you desire an optimal range below and above 0. E.g. imagine the min is -15 and the max 10, and you would like to have "nice" contours. What could then be an approach to define a nice amount of ticks automatically? E.g.np.linspace(-15,10,?). \$\endgroup\$pos_ticks = np.linspace(0, 10, x)andneg_ticks = np.linspace(0, -15, x), then just concatenate those two lists. Whether or not those two x's are the same thing may or may not matter depending on how you build the colormap \$\endgroup\$