Correcting matplotlib colorbar ticks

42,906

Solution 1

You are suffering from an off-by-one error. You have 10 ticklabels spread among 11 colors. You might be able to correct the error by using np.linspace instead of np.arange. Using np.linspace the third argument is the number of values desired. This reduces the amount of mental gymnastics needed to avoid the off-by-one error:

import matplotlib.pyplot as plt
import numpy as np
import matplotlib.cm as cm
import matplotlib.colors as mcolors

def colorbar_index(ncolors, cmap):
    cmap = cmap_discretize(cmap, ncolors)
    mappable = cm.ScalarMappable(cmap=cmap)
    mappable.set_array([])
    mappable.set_clim(-0.5, ncolors+0.5)
    colorbar = plt.colorbar(mappable)
    colorbar.set_ticks(np.linspace(0, ncolors, ncolors))
    colorbar.set_ticklabels(range(ncolors))

def cmap_discretize(cmap, N):
    """Return a discrete colormap from the continuous colormap cmap.

        cmap: colormap instance, eg. cm.jet. 
        N: number of colors.

    Example
        x = resize(arange(100), (5,100))
        djet = cmap_discretize(cm.jet, 5)
        imshow(x, cmap=djet)
    """

    if type(cmap) == str:
        cmap = plt.get_cmap(cmap)
    colors_i = np.concatenate((np.linspace(0, 1., N), (0.,0.,0.,0.)))
    colors_rgba = cmap(colors_i)
    indices = np.linspace(0, 1., N+1)
    cdict = {}
    for ki,key in enumerate(('red','green','blue')):
        cdict[key] = [ (indices[i], colors_rgba[i-1,ki], colors_rgba[i,ki])
                       for i in xrange(N+1) ]
    # Return colormap object.
    return mcolors.LinearSegmentedColormap(cmap.name + "_%d"%N, cdict, 1024)

fig, ax = plt.subplots()
A = np.random.random((10,10))*10
cmap = plt.get_cmap('YlGnBu')
ax.imshow(A, interpolation='nearest', cmap=cmap)
colorbar_index(ncolors=11, cmap=cmap)    
plt.show()

enter image description here

Solution 2

You can control the placement and the labels by hand. I'll start with a linear cmap generated from cmap_discretize on the page you linked:

import numpy as np
import pylab as plt

# The number of divisions of the cmap we have
k = 10

# Random test data
A = np.random.random((10,10))*k
c = cmap_discretize('jet', k)

# First show without
plt.subplot(121)
plt.imshow(A,interpolation='nearest',cmap=c)
plt.colorbar()

# Now label properly
plt.subplot(122)
plt.imshow(A,interpolation='nearest',cmap=c)

cb = plt.colorbar()
labels = np.arange(0,k,1)
loc    = labels + .5
cb.set_ticks(loc)
cb.set_ticklabels(labels)

plt.show()

enter image description here

Share:
42,906

Related videos on Youtube

urschrei
Author by

urschrei

Updated on July 09, 2022

Comments

  • urschrei
    urschrei almost 2 years

    I've placed a color bar alongside a choropleth map. Because the data being plotted are discrete rather than continuous values, I've used a LinearSegmentedColormap (using the recipe from the scipy cookbook), which I've initialised with my max counted value + 1, in order to show a colour for 0. However, I now have two problems:

    enter image description here

    1. The tick labels are incorrectly spaced (except for 5, more or less) – they should be located in the middle of the colour they identify; i.e. 0 - 4 should be moved up, and 6 - 10 should be moved down.

    2. If I initialise the colorbar with drawedges=True, so that I can style its dividers properties, I get this:

    enter image description here

    I'm creating my colormap and colorbar like so:

    cbmin, cbmax = min(counts), max(counts)
    # this normalises the counts to a 0,1 interval
    counts /= np.max(np.abs(counts), axis=0)
    # density is a discrete number, so we have to use a discrete color ramp/bar
    cm = cmap_discretize(plt.get_cmap('YlGnBu'), int(cbmax) + 1)
    mappable = plt.cm.ScalarMappable(cmap=cm)
    mappable.set_array(counts)
    # set min and max values for the colour bar ticks
    mappable.set_clim(cbmin, cbmax)
    pc = PatchCollection(patches, match_original=True)
    # impose our colour map onto the patch collection
    pc.set_facecolor(cm(counts))
    ax.add_collection(pc,)
    cb = plt.colorbar(mappable, drawedges=True)
    

    So I'm wondering whether my converting the counts to a 0,1 interval is one of the problems.

    Update :

    Having tried what Hooked suggested, the 0-value is correct, but subsequent values are set progressively higher, to the the point where 9 is where 10 should be:

    enter image description here

    Here's the code I used:

    cb = plt.colorbar(mappable)
    labels = np.arange(0, int(cbmax) + 1, 1)
    loc = labels + .5
    cb.set_ticks(loc)
    cb.set_ticklabels(labels)
    

    And just to confirm, labels definitely has the correct values:

    In [3]: np.arange(0, int(cbmax) + 1, 1)
    Out[3]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10])
    
    • Hooked
      Hooked over 10 years
      It seems like the problem must lie with the colorbar you essentially create "by hand" using a PatchCollection. Is there any reason not to use a linear discrete colormap like the scipy page you linked?
    • urschrei
      urschrei over 10 years
      You mean create the colormap, pass it PatchCollection as a cmap argument, and then pc.set_array to the counts?
    • Hooked
      Hooked over 10 years
      No I mean use cmap_discretize as on the page wiki.scipy.org/Cookbook/Matplotlib/ColormapTransformations . This is how I generated my example which doesn't suffer from the strange placement.
  • urschrei
    urschrei over 10 years
    Hmm, it's still not correct, but in a different way. See edit.