262

If you have a Colormap cmap, for example:

cmap = matplotlib.cm.get_cmap('Spectral')

How can you get a particular colour out of it between 0 and 1, where 0 is the first colour in the map and 1 is the last colour in the map?

Ideally, I would be able to get the middle colour in the map by doing:

>>> do_some_magic(cmap, 0.5) # Return an RGBA tuple
(0.1, 0.2, 0.3, 1.0)
FObersteiner
  • 22,500
  • 8
  • 42
  • 72
LondonRob
  • 73,083
  • 37
  • 144
  • 201

7 Answers7

394

You can do this with the code below, and the code in your question was actually very close to what you needed, all you have to do is call the cmap object you have.

import matplotlib

cmap = matplotlib.cm.get_cmap('Spectral')

rgba = cmap(0.5)
print(rgba) # (0.99807766255210428, 0.99923106502084169, 0.74602077638401709, 1.0)

For values outside of the range [0.0, 1.0] it will return the under and over colour (respectively). This, by default, is the minimum and maximum colour within the range (so 0.0 and 1.0). This default can be changed with cmap.set_under() and cmap.set_over().

For "special" numbers such as np.nan and np.inf the default is to use the 0.0 value, this can be changed using cmap.set_bad() similarly to under and over as above.

Finally it may be necessary for you to normalize your data such that it conforms to the range [0.0, 1.0]. This can be done using matplotlib.colors.Normalize simply as shown in the small example below where the arguments vmin and vmax describe what numbers should be mapped to 0.0 and 1.0 respectively.

import matplotlib

norm = matplotlib.colors.Normalize(vmin=10.0, vmax=20.0)

print(norm(15.0)) # 0.5

A logarithmic normaliser (matplotlib.colors.LogNorm) is also available for data ranges with a large range of values.

(Thanks to both Joe Kington and tcaswell for suggestions on how to improve the answer.)

JohanC
  • 71,591
  • 8
  • 33
  • 66
Ffisegydd
  • 51,807
  • 15
  • 147
  • 125
  • 4
    Actually, for values less than 0 or more than 1 it will return the "over" or "under" color. By default it's the color at the bottom/top of the colormap, but that's changable. For example: `cmap.set_under('red'); print cmap(0.0), cmap(-0.01)` – Joe Kington Aug 20 '14 at 15:55
  • Hi @Joe, thanks for the correction, I've modified my answer :) – Ffisegydd Aug 20 '14 at 15:58
  • There is also `set_bad` which define what in does for `np.nan` and `np.inf` iirc. You should mention the `Normalize` methods here as well. – tacaswell Aug 20 '14 at 18:31
  • @tcaswell thanks for the suggestions, have added them as well as an example of `matplotlib.colors.Normalize`. – Ffisegydd Aug 20 '14 at 18:51
  • 24
    Very useful information and why on earth is it impossible to find this in the documentation!?! – Jaap Eldering Jan 08 '17 at 19:55
  • when I use default range, and I tried values above 1, it does not return a single "over" number, rather, the value changes with my input. I'm using matplotlib 2.0.0 – Pengyao Mar 28 '17 at 20:58
  • 20
    If this isn't working for anyone, and you see `module 'matplotlib' has no attribute 'cm'`, try replacing the first two lines with `import matplotlib.pyplot as plt; cmap = plt.cm.get_cmap('Spectral')` – pretzlstyle Jul 20 '18 at 20:41
  • Does normalize have an equivalent to `set_bad`? – akozi Nov 27 '18 at 18:03
  • @Pengyao in my version of matplotlib the behavior depends on whether the input is an integer or a float. In the answer, a float is used. When specifying integers, it's different. Consult the section `_call_(self, X, ...)` in `help(cmap)` to get an explanation: "For floats, X should be in the interval ``[0.0, 1.0]`` to return the RGBA values ``X*100`` percent along the Colormap line. For integers, X should be in the interval ``[0, Colormap.N)`` to return RGBA values *indexed* from the Colormap with index ``X``." – Bastian Aug 22 '19 at 11:07
  • @Ffisegydd it may be worth pointing towards the different behaviors depending on whether a float or an integer is passed to the cmap function – Bastian Aug 22 '19 at 11:10
  • `import matplotlib.cm` also resolved `module 'matplotlib' has no attribute 'cm'` as mentioned here https://stackoverflow.com/a/52255079/6265439 – Sam Murphy Mar 30 '21 at 18:43
  • why is it so damn hard to work with color maps in matplotlib? It's so much easier in Matlab which matplotlib tries to mimic. – Sia Jun 30 '22 at 23:35
  • 1
    Since this way is getting deprecated, here's an up-to-date one: `import matplotlib` `cmap = matplotlib.colormaps["Spectral"]` – Gleb Ryabov Aug 04 '23 at 09:09
24

In order to get rgba integer value instead of float value, we can do

rgba = cmap(0.5,bytes=True)

So to simplify the code based on answer from Ffisegydd, the code would be like this:

#import colormap
from matplotlib import cm

#normalize item number values to colormap
norm = matplotlib.colors.Normalize(vmin=0, vmax=1000)

#colormap possible values = viridis, jet, spectral
rgba_color = cm.jet(norm(400),bytes=True) 

#400 is one of value between 0 and 1000
amaliammr
  • 241
  • 2
  • 4
13

I once ran into a similar situation where I needed "n" no. of colors from a colormap so that I can assign each color to my data. I have compiled a code to this in a package called "mycolorpy". You can pip install it using:

pip install mycolorpy

You can then do:

from mycolorpy import colorlist as mcp
import numpy as np

Example: To create a list of 5 hex strings from cmap "winter"

color1=mcp.gen_color(cmap="winter",n=5)
print(color1)

Output:

['#0000ff', '#0040df', '#0080bf', '#00c09f', '#00ff80']

Another example to generate 16 list of colors from cmap bwr:

color2=mcp.gen_color(cmap="bwr",n=16)
print(color2)

Output:

['#0000ff', '#2222ff', '#4444ff', '#6666ff', '#8888ff', '#aaaaff', '#ccccff', '#eeeeff', '#ffeeee', '#ffcccc', '#ffaaaa', '#ff8888', '#ff6666', '#ff4444', '#ff2222', '#ff0000']

There is a python notebook with usage examples to better visualize this.

Say you want to generate a list of colors from a cmap that is normalized to a given data. You can do that using:

a=random.randint(1000, size=(200))
a=np.array(a)
color1=mcp.gen_color_normalized(cmap="seismic",data_arr=a)
plt.scatter(a,a,c=color1)

Output: enter image description here

You can also reverse the color using:

color1=mcp.gen_color_normalized(cmap="seismic",data_arr=a,reverse=True)
plt.scatter(a,a,c=color1)

Output: enter image description here

Binod
  • 397
  • 4
  • 5
  • 2
    This does not answer the OP's question, while it already has an excellent approved answer. Added to this is that there are already tools out there that do a great job at the performing the process you describe, like CMasher (https://cmasher.readthedocs.io/index.html). – 1313e Nov 30 '21 at 11:05
  • 2
    @1313e: The "excellent approved answer" doesn't work anymore. So there's that. Thanks for the link to this excellent library, though. – Eric Duminil Jan 20 '22 at 07:07
  • 2
    Thank you so much! Saved my day. – seizethedata Feb 08 '22 at 17:13
3

I had precisely this problem, but I needed sequential plots to have highly contrasting color. I was also doing plots with a common sub-plot containing reference data, so I wanted the color sequence to be consistently repeatable.

I initially tried simply generating colors randomly, reseeding the RNG before each plot. This worked OK (commented-out in code below), but could generate nearly indistinguishable colors. I wanted highly contrasting colors, ideally sampled from a colormap containing all colors.

I could have as many as 31 data series in a single plot, so I chopped the colormap into that many steps. Then I walked the steps in an order that ensured I wouldn't return to the neighborhood of a given color very soon.

My data is in a highly irregular time series, so I wanted to see the points and the lines, with the point having the 'opposite' color of the line.

Given all the above, it was easiest to generate a dictionary with the relevant parameters for plotting the individual series, then expand it as part of the call.

Here's my code. Perhaps not pretty, but functional.

from matplotlib import cm
cmap = cm.get_cmap('gist_rainbow')  #('hsv') #('nipy_spectral')

max_colors = 31   # Constant, max mumber of series in any plot.  Ideally prime.
color_number = 0  # Variable, incremented for each series.

def restart_colors():
    global color_number
    color_number = 0
    #np.random.seed(1)

def next_color():
    global color_number
    color_number += 1
    #color = tuple(np.random.uniform(0.0, 0.5, 3))
    color = cmap( ((5 * color_number) % max_colors) / max_colors )
    return color

def plot_args():  # Invoked for each plot in a series as: '**(plot_args())'
    mkr = next_color()
    clr = (1 - mkr[0], 1 - mkr[1], 1 - mkr[2], mkr[3])  # Give line inverse of marker color
    return {
        "marker": "o",
        "color": clr,
        "mfc": mkr,
        "mec": mkr,
        "markersize": 0.5,
        "linewidth": 1,
    }

My context is JupyterLab and Pandas, so here's sample plot code:

restart_colors()  # Repeatable color sequence for every plot

fig, axs = plt.subplots(figsize=(15, 8))
plt.title("%s + T-meter"%name)

# Plot reference temperatures:
axs.set_ylabel("°C", rotation=0)
for s in ["T1", "T2", "T3", "T4"]:
    df_tmeter.plot(ax=axs, x="Timestamp", y=s, label="T-meter:%s" % s, **(plot_args()))

# Other series gets their own axis labels
ax2 = axs.twinx()
ax2.set_ylabel(units)

for c in df_uptime_sensors:
    df_uptime[df_uptime["UUID"] == c].plot(
        ax=ax2, x="Timestamp", y=units, label="%s - %s" % (units, c), **(plot_args())
    )

fig.tight_layout()
plt.show()

The resulting plot may not be the best example, but it becomes more relevant when interactively zoomed in. uptime + T-meter

BobC
  • 345
  • 2
  • 10
1

To build on the solutions from Ffisegydd and amaliammr, here's an example where we make CSV representation for a custom colormap:

#! /usr/bin/env python3
import matplotlib
import numpy as np 

vmin = 0.1
vmax = 1000

norm = matplotlib.colors.Normalize(np.log10(vmin), np.log10(vmax))
lognum = norm(np.log10([.5, 2., 10, 40, 150,1000]))

cdict = {
    'red':
    (
        (0., 0, 0),
        (lognum[0], 0, 0),
        (lognum[1], 0, 0),
        (lognum[2], 1, 1),
        (lognum[3], 0.8, 0.8),
        (lognum[4], .7, .7),
    (lognum[5], .7, .7)
    ),
    'green':
    (
        (0., .6, .6),
        (lognum[0], 0.8, 0.8),
        (lognum[1], 1, 1),
        (lognum[2], 1, 1),
        (lognum[3], 0, 0),
        (lognum[4], 0, 0),
    (lognum[5], 0, 0)
    ),
    'blue':
    (
        (0., 0, 0),
        (lognum[0], 0, 0),
        (lognum[1], 0, 0),
        (lognum[2], 0, 0),
        (lognum[3], 0, 0),
        (lognum[4], 0, 0),
    (lognum[5], 1, 1)
    )
}


mycmap = matplotlib.colors.LinearSegmentedColormap('my_colormap', cdict, 256)   
norm = matplotlib.colors.LogNorm(vmin, vmax)
colors = {}
count = 0
step_size = 0.001
for value in np.arange(vmin, vmax+step_size, step_size):
    count += 1
    print("%d/%d %f%%" % (count, vmax*(1./step_size), 100.*count/(vmax*(1./step_size))))
    rgba = mycmap(norm(value), bytes=True)
    color = (rgba[0], rgba[1], rgba[2])
    if color not in colors.values():
        colors[value] = color

print ("value, red, green, blue")
for value in sorted(colors.keys()):
    rgb = colors[value]
    print("%s, %s, %s, %s" % (value, rgb[0], rgb[1], rgb[2]))
Morten
  • 2,148
  • 2
  • 15
  • 16
1

Colormaps come with their own normalize method, so if you have a plot already made you can access the color at a certain value.

import matplotlib.pyplot as plt
import numpy as np

cmap = plt.cm.viridis

cm = plt.pcolormesh(np.random.randn(10, 10), cmap=cmap)

print(cmap(cm.norm(2.2)))
nedlrichards
  • 322
  • 2
  • 14
-3

For a quick and dirty you can use the map directly. Or you can just do what @amaliammr says.

data_size = 23   # range 0..23
colors = plt.cm.turbo
color_normal = colours.N/data_size

for i in range(data_size):
    col = colours.colors[int(i*color_normal)]


Konchog
  • 1,920
  • 19
  • 23