Extracting Segmented Colormap Boundary Locations

Viewed 403

I am working on making a custom colormap in Matplotlib.

I understand that with a color bar we have a continuous range of values mapped to a "spectrum" of colors that are defined in a color map. I am making a custom segmented colormap to cover a discrete range of values.

My code:

colors = ['#FBE9BD', '#F2C370', '#E58B48', '#C35D30', '#A12F29']
cmap1 = LinearSegmentedColormap.from_list("mycmap", colors, N=5)

The data that I am coloring ranges in values from [50, 450]. My understanding is that this color bar will then partition this range (max - min) into five equally spaced subsections and assign it a color value based on the array provided above. My question is, is there any easy way to extract the boundary ranges from this colormap? I would like to map the values of each bin range (i.e. 50-120, 121-175) to a legend that appears next to its respective color.

Second question - say we increase the value of N=5 to N=10. Without providing any additional colors in the colors array. Now we would have more subcategories than different color values. What happens here, do some bins share the same color assignment?

Any links to information or examples would be greatly appreciated.

1 Answers

The following code and plot try to illustrate what is happening.

LinearSegmentedColormap.from_list("", colors) internally creates a smooth colormap with in this case 5 colors at values 0, 0.25, 0.5, 0.75 and 1 (i.e. the range 0-1 evenly spread out into 5-1=4 equal spaces. The in-between positions are interpolated smoothly. When you set an N, N regions are put into the range 0-1. For example with N=10, 10 regions are set (using 11 boundaries). The colorvalue of each region is obtained by dividing the smooth colorrange between 0-1 into N (so, not N+1) equal positions.

Now, to map external values 50-450 to the internal values 0-1 a norm is used. Default, the minimum and maximum values of the data are used. vmin and/or vmax can set explicit minimum and maximum values. (Apart from that, also an explicit norm function can be assigned.)

To calculate the boundaries for the external values, the range 50-450 needs to be cut into N equal regions, so N+1 bounds. np.linspace(50, 450, N+1) is a numpy function that creates these boundary values into an array.

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

colors = ['#FBE9BD', '#F2C370', '#E58B48', '#C35D30', '#A12F29']
cmap1 = LinearSegmentedColormap.from_list("mycmap1", colors, N=10)

minval = 50
maxval = 450
bounds = np.linspace(minval, maxval, cmap1.N + 1)
x = np.random.uniform(size=1000)
y = np.random.uniform(minval, maxval, size=x.size)
plt.scatter(x, y, c=y, vmin=minval, vmax=maxval, cmap=cmap1)

cbar = plt.colorbar(ScalarMappable(cmap=LinearSegmentedColormap.from_list("mycmap1", colors, N=256)),
                    label='Complete colormap', ticks=np.linspace(0, 1, len(colors)))
cbar.ax.set_yticklabels(colors)
plt.colorbar(ticks=bounds, label=f'LinearSegmentedColormap N={cmap1.N}')
plt.show()

At the left N=5, at the right N=10.

illustrating plot

The following plot shows the same situation, but with more distinguishable colors. The thick marks on the smooth colorbar show where the exact colors for the segmented colorbar originate.

more diverse colors

PS: To create a legend in a similar way:

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

colors = ['#FBE9BD', '#F2C370', '#E58B48', '#C35D30', '#A12F29']

cmap1 = LinearSegmentedColormap.from_list("mycmap1", colors, N=10)

minval = 50
maxval = 450
bounds = np.linspace(minval, maxval, cmap1.N + 1)
x = np.random.uniform(size=1000)
y = np.random.uniform(minval, maxval, size=x.size)
plt.scatter(x, y, c=y, vmin=minval, vmax=maxval, cmap=cmap1)

handles = [Line2D([], [], color=cmap1(i / (cmap1.N - 1)),
                  marker='o', ls='', label=f'{bounds[i]:.0f}-{bounds[i+1]:.0f}')
           for i in range(cmap1.N) if i < 6 or i == cmap1.N-1]
plt.legend(handles=handles, bbox_to_anchor=[1.02, 1.02], loc='upper left')
plt.tight_layout()
plt.show()

legend example

Related