Matplotlib Discrete Colorbar

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.

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 create a colormap and a colorbar with discrete values in python?

The first figure shows 10 colors, so 11 boundaries are needed. The code below temporarily adds an extra boundary, but doesn't display its tick label. cbar.ax.set_title() is used to add text on top of the colorbar. When working with a BoundaryNorm, the ListedColormap can be created without providing tuples.

To set the ticks and their labels at the left of the colorbar, cbar.ax.tick_params can be used. Some extra padding is needed, which can be added via fig.colorbar(..., padding=).

The example code uses a scatterplot to test the colorbar

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

RR = [0, 0, 70, 44, 255, 255, 255, 255, 128, 255]
GG = [255, 0, 220, 141, 255, 200, 142, 0, 0, 153]
BB = [255, 255, 45, 29, 75, 50, 0, 0, 128, 255]

colors = np.c_[RR, GG, BB] / 255
my_colormap = clr.LinearSegmentedColormap.from_list('RADAR', colors)
VariableLimits = np.array([1, 5, 10, 20, 30, 40, 50, 65, 80, 100])
norm = clr.BoundaryNorm(np.append(VariableLimits, 1000), ncolors=256)

fig, ax = plt.subplots()
pm = ax.scatter(np.random.rand(100), np.random.rand(100), c=np.random.uniform(0, 120, 100),
cmap=my_colormap, norm=norm)
cbar = fig.colorbar(pm, ticks=VariableLimits, pad=0.1, ax=ax)
cbar.ax.set_title('(mm)', size=8)
cbar.ax.tick_params(left=True, right=False, labelleft=True, labelright=False)

plt.show()

colorbar with variable bounds

Custom Spacing for colors in discrete colorbar

The following approach uses a BoundaryNorm and proportional spacing for the colorbar:

from matplotlib import pyplot as plt
from matplotlib.colors import BoundaryNorm, ListedColormap
import numpy as np
import seaborn as sns

# make colormap
yellow = (249 / 255, 231 / 255, 85 / 255)
blue = (62 / 255, 11 / 255, 81 / 255)
color_list = [yellow, blue]
cmap = ListedColormap(color_list)
# define a boundary norm
proportion_class0 = 0.67 # proportion for class0
norm = BoundaryNorm([0, proportion_class0, 1], 2)

binary_preds = np.random.choice([False, True], size=(10, 15), p=[proportion_class0, 1 - proportion_class0])
ax = sns.heatmap(binary_preds, cmap=cmap, norm=norm,
cbar_kws=dict(use_gridspec=False, location="left", spacing="proportional"))

for i in range(len(binary_preds) + 1):
ax.axhline(i, color='white', lw=5)

colorbar = ax.collections[0].colorbar
colorbar.set_ticks([proportion_class0 / 2, (1 + proportion_class0) / 2])
colorbar.set_ticklabels(['Class0', 'Class1'])

plt.show()

BoundaryNorm to change colorbar spacing

Matplotlib: Discrete colorbar fails for custom labels

The buoundaries need to be in data units. Meaning, if your classes are 4,5,6, you probably want to use boundaries of 3.5, 4.5, 5.5, 6.5.

import matplotlib.pyplot as plt
import numpy as np

datapoints = np.array([[1,1],[2,2],[3,3],[4,4],[5,5],[6,6],[7,7]])
labels = np.array([4,5,6,4,5,6,4])
classes = np.array([4,5,6])

fig, ax = plt.subplots(1 , figsize=(6, 6))
sc = ax.scatter(datapoints[:,0], datapoints[:,1], s=20, c=labels, cmap='jet', alpha=1.0)
ax.set(xticks=[], yticks=[])
cbar = plt.colorbar(sc, ticks=classes, boundaries=np.arange(4,8)-0.5)

plt.show()

Sample Image

If you wanted to have the boundaries determined automatically from the classes, some assumption must me made. E.g. if all classes are subsequent integers,

boundaries=np.arange(classes.min(), classes.max()+2)-0.5

In general, an alternative would be to use a BoundaryNorm, as shown e.g. in Create a discrete colorbar in matplotlib
or How to specify different color for a specific year value range in a single figure? (Python) or python colormap quantisation (matplotlib)

discrete colorbar with discrete colormesh

You can use a BoundaryNorm to set where the boundaries are. Also, you don't need to include green as a colour in the list of colours. For example:

bounds =  [-5, Z_1, Z_2, Z_3]    
cmap = colors.ListedColormap(['white', 'black', 'red']).with_extremes(over='green')
norm = colors.BoundaryNorm(bounds, cmap.N)

pm = plt.pcolormesh(my_matrix.T, cmap=cmap, norm=norm)
plt.colorbar(pm, extend='max', extendfrac='auto')

Sample Image

Partially discrete colormap matplotlib

Alright, I figured out a solution to my own problem. I adapted the solution from HERE to address my issue. Below is the code to accomplish this.

Setup:

import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
from matplotlib.cm import get_cmap

y = np.linspace(-3, 3, 100)
x = np.linspace(-3, 3, 100)
z = np.zeros(shape=(x.shape[0], y.shape[0]))

x_mat, y_mat = np.meshgrid(x, y)

# Threshold to apply
z_threshold = 1e-6

for i in range(x.shape[0]):
z[:, i] = norm.pdf(y, loc=0, scale=0.2+(i/100))
z[:, i] = z[:, i] / np.sum(z[:, i]) # normalizing

Next I define two different colormaps. The first color map applies to all values above the threshold. If values are below the threshold, it sets that square as transparent.

cmap = get_cmap('terrain')
# 1.8 and 0.2 are used to restrict the upper and lower parts of the colormap
colors = cmap((z - z_threshold) / ((np.max(z)*1.8) - (np.min(z))) + 0.2)
# if below threshold, set as transparent (alpha=0)
colors[z < z_threshold, -1] = 0

The second colormap defines the color for all places below the threshold. This step isn't fully necessary, but it does prevent the plane from being drawn below the rest of the plot.

colors2 = cmap(z)
colors2[z >= z_threshold, -1] = 0

Now the colormaps can be used in two 3D plot calls

# init 3D plot
f1 = plt.axes(projection='3d')
# Plot values above the threshold
f1.plot_surface(x_mat, y_mat, z, facecolors=colors, edgecolor='none', rstride=1)
# Plot values below the threshold
z_below = np.zeros(shape=(x.shape[0], y.shape[0]))
f1.plot_surface(x_mat, y_mat, z_below,
facecolors=colors2, edgecolor='none', rstride=1, vmin=0)
# Setting the zlimits
f1.set_zlim([0, np.max(z)])
plt.show()

The above results in the following plot
Sample Image



Related Topics



Leave a reply



Submit