'matplotlib LineCollection misinterprets colormap

I want to produce a line plot with matplotlib.pyplot where each line segment has its assigned color. A color can occur more than once. I use a LineCollection and its attribute cmap (short for "colormap") to accomplish this, but encounter a problem.
I hope the following code example is reduced enough. The array data is just an example, as well as the (in this case periodic) color assignments.

import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
import numpy as np

n_colors = 8
n_datapoints = 60

# plot legend
data_x = range(n_colors)
data_color = range(n_colors)
fig, ax = plt.subplots(figsize=(10, 2))
rects = ax.bar(data_x, [1]*n_colors, color=plt.cm.tab20(data_color), alpha = 1)
plt.xticks(data_x)

fig, ax = plt.subplots(
    nrows=1,
    figsize=(15,10),
    squeeze=True,
)

# initialize data to plot
data = np.zeros((n_datapoints,))
for i in range(n_datapoints):
    data[i] = i*i % 20

# prepare data for use in LineCollection
x_y_values = np.vstack((np.arange(n_datapoints), data)).T
x_y_values = x_y_values.reshape(-1,1,2)
segments = np.hstack([x_y_values[:-1], x_y_values[1:]])

# colors for line segments
colors = np.array(list(map(lambda n: n % n_colors, range(n_datapoints))))

# plot

lineCollection = LineCollection(
    segments=segments,
    cmap=plt.cm.tab20,
    **{"linewidths": 1, "alpha": 1},
)
lineCollection.set_array(colors)
ax.add_collection(lineCollection)
ax.autoscale_view()

plt.show()

This produces the following two plots:

legend when n_colors==8 plot when n_colors==8

As you can see (well, I hope it is visible for you), the colors in the plot do not match the colors in the legend. I expect colors blue, light-blue, orange, light-orange, green, light-green, red, light-red in this order, repeating after the end. What I get is blue, orange, light-green, purple, brown, color-I-don't-know, light-green, light-blue, repeating after the end. Particularly, notice that some of the colors in the plot (brown, for example) are not within the first 8 colors of the colormap plt.cm.tab20.
How can this be? Why are the line segments not colored correctly?

When I changed line 6 of the above code from n_colors = 8 to n_colors = 5, the legend still showed what I would expect. Yet the colors of the line plot changed:

legend when n_colors==5 plot when n_colors==5

The plot now shows colors blue, light-green, brown, gray, light-blue. So it seems to me that plt.cm.tab20 is not really a discrete and fixed colormap. But a discrete and fixed colormap is what I want.
What am I doing wrong? How can I get a consistent mapping from {integer between 0 and 20} to {color} for my line plots?



Solution 1:[1]

It seems that matplotlib's LineCollection scales the passed array colors onto the real interval [0, 1]. So if only the numbers 0, ..., 7 appear in it, then (I suppose) I get 8 colors from tab20 which are more or less equally spaced among all 20 colors.
By passing the keyword argument norm (documentation can be found here) to LineCollection, this can be remedied:

lineCollection = LineCollection(
    segments=segments,
    cmap=plt.cm.tab20,
    norm=mpl.colors.Normalize(vmin=0, vmax=19),
    **{"linewidths": 1, "alpha": 1},
)
lineCollection.set_array(colors)

Here I added the line norm=mpl.colors.Normalize(vmin=0, vmax=19), (knowing that my preferred colormap uses indices from 0 to 19).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 NerdOnTour