Defining a Discrete Colormap for Imshow in Matplotlib

Defining a discrete colormap for imshow in matplotlib

You can use a ListedColormap to specify the white and red as the only colors in the color map, and the bounds determine where the transition is from one color to the next:

import matplotlib.pyplot as plt
from matplotlib import colors
import numpy as np

np.random.seed(101)
zvals = np.random.rand(100, 100) * 10

# make a color map of fixed colors
cmap = colors.ListedColormap(['white', 'red'])
bounds=[0,5,10]
norm = colors.BoundaryNorm(bounds, cmap.N)

# tell imshow about color map so that only set colors are used
img = plt.imshow(zvals, interpolation='nearest', origin='lower',
cmap=cmap, norm=norm)

# make a color bar
plt.colorbar(img, cmap=cmap, norm=norm, boundaries=bounds, ticks=[0, 5, 10])

plt.savefig('redwhite.png')
plt.show()

The resulting figure has only two colors:

Sample Image

I proposed essentially the same thing for a somewhat different question: 2D grid data visualization in Python

The solution is inspired by a matplotlib example. The example explains that the bounds must be one more than the number of colors used.

The BoundaryNorm is a normalization that maps a series of values to integers, which are then used to assign the corresponding colors. cmap.N, in the example above, just defines the number of colors.

Matplotlib discrete colorbar

You can create a custom discrete colorbar quite easily by using a BoundaryNorm as normalizer for your scatter. The quirky bit (in my method) is making 0 showup as grey.

For images i often use the cmap.set_bad() and convert my data to a numpy masked array. That would be much easier to make 0 grey, but i couldnt get this to work with the scatter or the custom cmap.

As an alternative you can make your own cmap from scratch, or read-out an existing one and override just some specific entries.

import numpy as np
import matplotlib as mpl
import matplotlib.pylab as plt

fig, ax = plt.subplots(1, 1, figsize=(6, 6)) # setup the plot

x = np.random.rand(20) # define the data
y = np.random.rand(20) # define the data
tag = np.random.randint(0, 20, 20)
tag[10:12] = 0 # make sure there are some 0 values to show up as grey

cmap = plt.cm.jet # define the colormap
# extract all colors from the .jet map
cmaplist = [cmap(i) for i in range(cmap.N)]
# force the first color entry to be grey
cmaplist[0] = (.5, .5, .5, 1.0)

# create the new map
cmap = mpl.colors.LinearSegmentedColormap.from_list(
'Custom cmap', cmaplist, cmap.N)

# define the bins and normalize
bounds = np.linspace(0, 20, 21)
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)

# make the scatter
scat = ax.scatter(x, y, c=tag, s=np.random.randint(100, 500, 20),
cmap=cmap, norm=norm)

# create a second axes for the colorbar
ax2 = fig.add_axes([0.95, 0.1, 0.03, 0.8])
cb = plt.colorbar.ColorbarBase(ax2, cmap=cmap, norm=norm,
spacing='proportional', ticks=bounds, boundaries=bounds, format='%1i')

ax.set_title('Well defined discrete colors')
ax2.set_ylabel('Very custom cbar [-]', size=12)

Sample Image

I personally think that with 20 different colors its a bit hard to read the specific value, but thats up to you of course.

Setting discrete colormap corresponding to specific data range in Matplotlib

There are various answers to other questions using ListedColormap and BoundaryNorm, but here's an alternative. I've ignored the placement of your colorbar, as that's not relevant to your question.

You can replace your binlabel calculation with a call to np.digitize() and replace your discrete_cmap() function by using the lut argument to get_cmap(). Also, I find it easier to place the color bounds at .5 midpoints between the indexes rather than scale to awkward fractions of odd numbers:

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

ratio = np.random.random((50,50)) * 50.0 - 20.0

fig2, ax2 = plt.subplots(figsize=(5,5))

# Turn the data into an array of N bin indexes (i.e., 0, 1 and 2).
bounds = [0,20]
iratio = np.digitize(ratio.flat,bounds).reshape(ratio.shape)

# Create a colormap containing N colors and a Normalizer that defines where
# the boundaries of the colors should be relative to the indexes (i.e., -0.5,
# 0.5, 1.5, 2.5).
cmap = cm.get_cmap("jet",lut=len(bounds)+1)
cmap_bounds = np.arange(len(bounds)+2) - 0.5
norm = mcol.BoundaryNorm(cmap_bounds,cmap.N)

# Plot using the colormap and the Normalizer.
ratio_plot = plt.pcolormesh(iratio,cmap=cmap,norm=norm)
cbar = plt.colorbar(ratio_plot,ticks=[0,1,2],orientation="horizontal")
cbar.set_ticklabels(["< 0","0~20",">20"])

Example discrete color bar

Create a discrete colorbar in matplotlib

Indeed, the fist argument to colorbar should be a ScalarMappable, which would be the scatter plot PathCollection itself.

Setup

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({"x" : np.linspace(0,1,20),
"y" : np.linspace(0,1,20),
"cluster" : np.tile(np.arange(4),5)})

cmap = mpl.colors.ListedColormap(["navy", "crimson", "limegreen", "gold"])
norm = mpl.colors.BoundaryNorm(np.arange(-0.5,4), cmap.N)

Pandas plotting

The problem is that pandas does not provide you access to this ScalarMappable directly. So one can catch it from the list of collections in the axes, which is easy if there is only one single collection present: ax.collections[0].

fig, ax = plt.subplots()
df.plot.scatter(x='x', y='y', c='cluster', marker='+', ax=ax,
cmap=cmap, norm=norm, s=100, edgecolor ='none', alpha=0.70, colorbar=False)

fig.colorbar(ax.collections[0], ticks=np.linspace(0,3,4))
plt.show()

Matplotlib plotting

One could consider using matplotlib directly to plot the scatter in which case you would directly use the return of the scatter function as argument to colorbar.

fig, ax = plt.subplots()
scatter = ax.scatter(x='x', y='y', c='cluster', marker='+', data=df,
cmap=cmap, norm=norm, s=100, edgecolor ='none', alpha=0.70)

fig.colorbar(scatter, ticks=np.linspace(0,3,4))
plt.show()

Output in both cases is identical.

Sample Image

how to define colormap with absolute values with matplotlib

The key in this case is the norm, not the colormap.

The colormap defines colors for already scaled data. The norm scales the data to a 0-1 range.

By default, a Normalize instance will be created that scales between the min and max of the data or the vmin and vmax kwargs, if they are supplied.

However, there are a few different helper functions that may be useful in your case.

If you want a discrete color bar, there's a helper function to generate both a norm and a cmap for you: matplotlib.colors.from_levels_and_colors It takes a list of values and a list of colors and returns a BoundaryNorm instance and a LinearSegmentedColormap instance:

For example:

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

data1 = 3 * np.random.random((10, 10))
data2 = 5 * np.random.random((10, 10))

levels = [0, 1, 2, 3, 4, 5]
colors = ['red', 'brown', 'yellow', 'green', 'blue']
cmap, norm = matplotlib.colors.from_levels_and_colors(levels, colors)

fig, axes = plt.subplots(ncols=2)
for ax, dat in zip(axes, [data1, data2]):
im = ax.imshow(dat, cmap=cmap, norm=norm, interpolation='none')
fig.colorbar(im, ax=ax, orientation='horizontal')
plt.show()

Sample Image

Note that this creates a discrete colormap.

If we wanted to use a continuous colormap instead, we can either specify the same vmin and vmax arguments or create our own Normalize instance and pass it in as the norm argument for all images.

Also, there's a similar function to create a continuous colormap from a list of colors:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap

data1 = 3 * np.random.random((10, 10))
data2 = 5 * np.random.random((10, 10))

colors = ['red', 'brown', 'yellow', 'green', 'blue']
cmap = LinearSegmentedColormap.from_list('name', colors)
norm = plt.Normalize(0, 5)

fig, axes = plt.subplots(ncols=2)
for ax, dat in zip(axes, [data1, data2]):
im = ax.imshow(dat, cmap=cmap, norm=norm, interpolation='none')
fig.colorbar(im, ax=ax, orientation='horizontal')
plt.show()

Sample Image

matplotlib imshow distorting colors

This worked for me:

plt.imshow(lena[:,:,::-1]) # RGB-> BGR

Same idea but nicer and more robust approach is to use "ellipsis" proposed by @rayryeng:

plt.imshow(lena[...,::-1])


Related Topics



Leave a reply



Submit