1

I'm looking for a solution to create a colorbar with uniform tick labels (equally spaced along the colorbar) even if the bounds are non-linear. Currently, as the ticks are proportionally spaced based on the values of the bounds, the top part of the colorbar is quite stretched and the base is so compressed it is impossible to see which colors corresponds to which value. I want to keep the same color/value combinations, but with a tick label spacing that makes the colorbar legible.

The colorbar I get with my current code:
1

Here's the code I used:

import matplotlib as mpl
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.colors import LinearSegmentedColormap
from matplotlib.figure import Figure

# data
bounds = [0.1, 0.25, 0.5, 1, 2.5, 5, 7.5, 10, 15, 20, 25, 50, 100]
style_color = [[0, 0, 127],
               [0, 0, 197],
               [0, 21, 254],
               [0, 126, 254],
               [0, 231, 254],
               [68, 253, 186],
               [153, 254, 101],
               [238, 254, 16],
               [254, 187, 0],
               [254, 101, 0],
               [254, 16, 0],
               [197, 0, 0],
               [127, 0, 0],
               [127, 0, 0]]

# transform color rgb value to 0-1 range
color_arr = []
for color in style_color:
    rgb = [float(value)/255 for value in color]
    color_arr.append(rgb)

# normalize bound values
norm = mpl.colors.Normalize(vmin=min(bounds), vmax=max(bounds))
normed_vals = norm(bounds)

# create a colormap
cmap = LinearSegmentedColormap.from_list(
    'my_palette',
    list(zip(normed_vals, color_arr[:-1])),
    N=256
    )
cmap.set_over([color for color in color_arr[-1]])
cmap.set_under([color for color in color_arr[0]])

# create a figure
fig = Figure(figsize=(2, 5))
canvas = FigureCanvasAgg(fig)
ax = fig.add_subplot(121)

# create the colorbar
cb = mpl.colorbar.ColorbarBase(ax,
                               cmap=cmap,
                               norm=norm,
                               extend='max',
                               ticks=bounds)

fig.savefig('non-linear_colorbar')
Zephyr
  • 11,891
  • 53
  • 45
  • 80

1 Answers1

1

A BoundaryNorm seems to be what you're looking for:

import matplotlib as mpl
from matplotlib.colors import LinearSegmentedColormap, BoundaryNorm
from matplotlib import pyplot as plt

# data
bounds = [0.1, 0.25, 0.5, 1, 2.5, 5, 7.5, 10, 15, 20, 25, 50, 100]
style_color = [[0, 0, 127],
               [0, 0, 197],
               [0, 21, 254],
               [0, 126, 254],
               [0, 231, 254],
               [68, 253, 186],
               [153, 254, 101],
               [238, 254, 16],
               [254, 187, 0],
               [254, 101, 0],
               [254, 16, 0],
               [197, 0, 0],
               [127, 0, 0],
               [127, 0, 0]]

# transform color rgb value to 0-1 range
color_arr = []
for color in style_color:
    rgb = [float(value) / 255 for value in color]
    color_arr.append(rgb)

# normalize bound values
norm = mpl.colors.BoundaryNorm(bounds, ncolors=256)

# create a colormap
cmap = LinearSegmentedColormap.from_list('my_palette', color_arr, N=256)

# create a figure
fig, ax = plt.subplots(figsize=(2, 5), gridspec_kw={'left': 0.4, 'right': 0.5})

# create the colorbar
cb = mpl.colorbar.ColorbarBase(ax, cmap=cmap, norm=norm, extend='max', ticks=bounds)
plt.show()

resulting plot

PS: If you need a smooth colorbar, you could stretch the bounds:

import numpy as np

bounds = [0.1, 0.25, 0.5, 1, 2.5, 5, 7.5, 10, 15, 20, 25, 50, 100]
stretched_bounds = np.interp(np.linspace(0, 1, 257), np.linspace(0, 1, len(bounds)), bounds)

# normalize stretched bound values
norm = mpl.colors.BoundaryNorm(stretched_bounds, ncolors=256)

# ....
cb = mpl.colorbar.ColorbarBase(ax, cmap=cmap, norm=norm, extend='max', ticks=bounds)

stretched bounds

PS: new_y = np.interp(new_x, old_x, old_y) interpolates new values for y by first looking up the x in the array of old x, and finding the corresponding old y. When the new x is in between two old x's, the new y will be proportionally in between the old y's.

For the BoundaryNorm, np.interp calculates all the in-between values to get 256 different levels instead of the original 13.

JohanC
  • 71,591
  • 8
  • 33
  • 66
  • I already looked into LogNorm and PowerNorm normalizations. LogNorm is good for this specific exemple of colorbar, but I need to generate colorbars for a large variety of datasets including datasets with negative values, which can't be normalized with LogNorm. PowerNorm needs a gamma value that is specific to each dataset, so it would be difficult to automate the colorbar generation process for all the datasets. The ideal solution would be one that is applicable no matter the distribution of the bound values and that can support negative values. – Marie-Eve LB Jul 24 '20 at 13:33
  • The smooth colorbar version at the bottom of the new answer is exactly what I was looking for, thank you for your help! Could you explain a bit the logic behind the way you generated the stretched bounds? – Marie-Eve LB Jul 24 '20 at 19:20
  • I added a bit of explanation, but it is quite hard to explain clearly. – JohanC Jul 24 '20 at 20:06