3

I am making a heatmap in seaborn. I am using 'viridis', but I modify it slightly so some of the values get particular colors. In my MWE, .set_over is used to set the values above 90 to 'black', and .set_under is used to set the values below 10 to 'white'. I also mask out part of the heatmap. This all works fine.

How can I also map a middle range value, 20, to 'orange', and without effecting the current colorbar appearance? As you can see, .set_over, and .set_under do not change the colorbar appearance.

import matplotlib
import seaborn as sns
import numpy as np
np.random.seed(7)
A = np.random.randint(0,100, size=(20,20))
mask_array = np.zeros((20, 20), dtype=bool)
mask_array[:, :5] = True
cmap = matplotlib.colormaps["viridis"]
# Set the under color to white
cmap.set_under("white")
# Set the voer color to white
cmap.set_over("black")
# Set the background color

g = sns.heatmap(A, vmin=10, vmax=90, cmap=cmap, mask=mask_array)
# Set color of masked region
g.set_facecolor('lightgrey')

enter image description here

I have seen Map value to specific color in seaborn heatmap, but I am not sure how I can use it to solve my problem.

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Simd
  • 19,447
  • 42
  • 136
  • 271

4 Answers4

3

Consider the following:

import matplotlib as mpl
import seaborn as sns
import numpy as np

A = np.random.randint(0,100, size=(20,20))
mask_array = np.zeros((20, 20), dtype=bool)
mask_array[:, :5] = True
cmap = mpl.colormaps["viridis"]

newcolors = cmap(np.linspace(0, 1, 100))
newcolors[:10] = np.array([1,1,1,1])
newcolors[90:] = np.array([0,0,0,1])
newcolors[20] = mpl.colors.to_rgb('tab:orange') + (1,)

newcmap = mpl.colors.ListedColormap(newcolors)

g = sns.heatmap(A, cmap=newcmap, mask=mask_array)
# Set color of masked region
g.set_facecolor('lightgrey')

Sample result:

enter image description here

The following (appended to the above script) will make it so that the colorbar has a visible outline.

cbar_ax = g.figure.axes[-1]

for spine in cbar_ax.spines.values():
    spine.set(visible=True)

Sample result with outline:

enter image description here


In order to mask the colors of the heatmap, but not show an updated colorbar, set cbar=False, and then attach a custom colorbar, as shown in Standalone colorbar.

g = sns.heatmap(A, cmap=newcmap, mask=mask_array, cbar=False)

# add a new axes of the desired shape
cb = g.figure.add_axes([0.93, 0.11, 0.025, 0.77])

# attach a new colorbar to the axes
mpl.colorbar.ColorbarBase(cb, cmap='viridis', norm=mpl.colors.Normalize(10, 90),  # vmax and vmin
                          label=None, ticks=range(10, 91, 10))

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Ben Grossmann
  • 4,387
  • 1
  • 12
  • 16
2

Pulling from this answer, here is a solution that uses a mask rather than a custom colorbar:

import matplotlib
import seaborn as sns
import numpy as np
from matplotlib.colors import ListedColormap

np.random.seed(7)
A = np.random.randint(0,100, size=(20,20))
mask_array = np.zeros((20, 20), dtype=bool)
mask_array[:, :5] = True
# cmap = matplotlib.colormaps["viridis"]
cmap = matplotlib.cm.get_cmap('viridis')


# Set the under color to white
cmap.set_under("white")

# Set the voer color to white
cmap.set_over("black")

# Set the background color

g = sns.heatmap(A, vmin=10, vmax=90, cmap=cmap, mask=mask_array)
# Set color of masked region
g.set_facecolor('lightgrey')

special_data = np.ma.masked_where(A==20, A)
sns.heatmap(special_data, cmap=ListedColormap(['orange']), 
            mask=(special_data != 1), cbar=False)
astroChance
  • 337
  • 1
  • 10
2
  • The difficulty arises from the requirement to use more than a single mask.
    • mask_array, and a mask for A == 20 if the array is only int, or 20 <= A < 21 if the array is float.
  • matplotlib.colors.Colormap offers only 3 methods for setting colors.
    1. set_under
    2. set_over
    3. set_bad - Set the color for masked values, which is already used for mask_array.
    • with_extremes - Does the three combined.
      • cmap = mpl.colormaps['viridis'].with_extremes(bad='orange', under='w', over='k')
      • enter image description here
    • Using these methods doesn't effect the look of the colorbar.

Imports and Sample Data

import numpy as np
import seaborn as sns
import matplotlib as mpl

# set for repeatable sample
np.random.seed(2023)

# random 20 x 20 array
# A = np.random.randint(0, 100, size=(20, 20))  # random integers
A = np.random.random(size=(20, 20)) * 100  # random floats

1. Don't show the sliced columns

  • This is the easiest option, because it frees up mask
# remove the unneeded columns
A = A[:, 5:]

# create the value mask
mask = np.logical_and(A >= 20, A < 21)

# create the colormap with extremes
cmap = mpl.colormaps["viridis"].with_extremes(bad='orange', under='w', over='k')

# plot
g = sns.heatmap(A, vmin=10, vmax=90, cmap=cmap, mask=mask)

# reset the xticklabels to show the correct column labels
_ = g.set_xticks(ticks=g.get_xticks(), labels=range(5, 20))

enter image description here

2. Show all the columns

  • This is the more cumbersome option because, like the answer provided by Ben, this requires manually adding a color to cmap, and adding a custom colorbar.
    • Using mask, or any np.nan values in the array, are colored by set_bad.
  • This reuses the colorbar creation method I previously added to Ben's answer, which came from Standalone colorbar.
  • As demonstrated in Creating Colormaps in Matplotlib, a new color can be added into a slice of colors from a resampled colormap.
    • This does not get the correct value if vmin and vmax are used, because those options change the range of the colorbar.
# create the column mask
mask = np.zeros((20, 20), dtype=bool)
mask[:, :5] = True  # A[:, :5] = np.nan has the same effect

# slice the colors into the range of values in the array
colors = mpl.colormaps["viridis"].resampled(100).colors

# map a specific value range to a color; use a range for floats, and / or set a hight number to resampled
colors[19:21] = mpl.colors.to_rgba('tab:orange')

# create the new colormap with extremes
cmap = mpl.colors.ListedColormap(colors).with_extremes(bad='lightgray', under='w', over='k')

# draw the heatmap
g = sns.heatmap(A, cmap=cmap, mask=mask, cbar=False)

# add a new axes of the desired shape
cb_ax = g.figure.add_axes([0.93, 0.11, 0.025, 0.77])

# attach a new colorbar to the axes without an outline
cb = mpl.colorbar.ColorbarBase(cb_ax, cmap='viridis', norm=mpl.colors.Normalize(10, 90),  # vmax and vmin
                               label=None, ticks=range(10, 91, 10)).outline.set_visible(False)

enter image description here

3. Let the columns and bad values share the color

  • enter image description here
# create the value mask
mask = np.logical_and(A >= 20, A < 21)

# add the unwanted column to the mask as np.nan
mask[:, :5] = np.nan

# create the colormap with extremes
cmap = mpl.colormaps["viridis"].with_extremes(bad='orchid', under='w', over='k')

# plot
g = sns.heatmap(A, vmin=10, vmax=90, cmap=cmap, mask=mask)

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
0

create custom colormap matplotlib.colors.LinearSegmentedColormap.from_list . here isn't a direct way to set a specific value (like 20) to a certain color (like orange) when using a continuous colormap like 'viridis'.

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

np.random.seed(7)
A = np.random.randint(0,100, size=(20,20))
mask_array = np.zeros((20, 20), dtype=bool)
mask_array[:, :5] = True

# Create a colormap for each part
colormap_under_10 = plt.cm.viridis(np.linspace(0, 0.1, 10))
colormap_10_30 = plt.cm.Oranges(np.linspace(0.5, 1, 20))
colormap_above_30 = plt.cm.viridis(np.linspace(0.1, 1, 70))

# Combine them and build a new colormap
colors_combined = np.vstack((colormap_under_10, colormap_10_30, colormap_above_30))
mymap = colors.LinearSegmentedColormap.from_list('colormap_combined', colors_combined)

ax = sns.heatmap(A, vmin=0, vmax=100, cmap=mymap, mask=mask_array)

# Set color of masked region
ax.set_facecolor('lightgrey')

plt.show()

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
suraj sharma
  • 415
  • 4
  • 14
  • This isn't what I was looking for. The answer of @BenGrossmann is perfect except for the colorbar. – Simd Jun 11 '23 at 18:12