Skip to content

Commit 5d219d0

Browse files
authored
Merge pull request #30824 from ayshih/segmentedbivarcolormap_fix
Fixed bilinear interpolation for `SegmentedBivarColormap`
1 parent 418a816 commit 5d219d0

File tree

6 files changed

+115
-83
lines changed

6 files changed

+115
-83
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Bivariate colormaps now fully span the intended range of colors
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
Bivariate colormaps generated by ``SegmentedBivarColormap`` (e.g., ``BiOrangeBlue``)
4+
from a set of input colors now fully span that range of colors. There had been a bug
5+
with the numerical interpolation such that the colormap did not actually include the
6+
first or last colors.

lib/matplotlib/_cm_bivar.py

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
# auto-generated by https://github.com/trygvrad/multivariate_colormaps
2-
# date: 2024-05-24
3-
41
import numpy as np
52
from matplotlib.colors import SegmentedBivarColormap
63

4+
# auto-generated by https://github.com/trygvrad/multivariate_colormaps
5+
# date: 2024-05-24
76
BiPeak = np.array(
87
[0.000, 0.674, 0.931, 0.000, 0.680, 0.922, 0.000, 0.685, 0.914, 0.000,
98
0.691, 0.906, 0.000, 0.696, 0.898, 0.000, 0.701, 0.890, 0.000, 0.706,
@@ -1276,32 +1275,9 @@
12761275
]).reshape((65, 65, 3))
12771276

12781277
BiOrangeBlue = np.array(
1279-
[0.000, 0.000, 0.000, 0.000, 0.062, 0.125, 0.000, 0.125, 0.250, 0.000,
1280-
0.188, 0.375, 0.000, 0.250, 0.500, 0.000, 0.312, 0.625, 0.000, 0.375,
1281-
0.750, 0.000, 0.438, 0.875, 0.000, 0.500, 1.000, 0.125, 0.062, 0.000,
1282-
0.125, 0.125, 0.125, 0.125, 0.188, 0.250, 0.125, 0.250, 0.375, 0.125,
1283-
0.312, 0.500, 0.125, 0.375, 0.625, 0.125, 0.438, 0.750, 0.125, 0.500,
1284-
0.875, 0.125, 0.562, 1.000, 0.250, 0.125, 0.000, 0.250, 0.188, 0.125,
1285-
0.250, 0.250, 0.250, 0.250, 0.312, 0.375, 0.250, 0.375, 0.500, 0.250,
1286-
0.438, 0.625, 0.250, 0.500, 0.750, 0.250, 0.562, 0.875, 0.250, 0.625,
1287-
1.000, 0.375, 0.188, 0.000, 0.375, 0.250, 0.125, 0.375, 0.312, 0.250,
1288-
0.375, 0.375, 0.375, 0.375, 0.438, 0.500, 0.375, 0.500, 0.625, 0.375,
1289-
0.562, 0.750, 0.375, 0.625, 0.875, 0.375, 0.688, 1.000, 0.500, 0.250,
1290-
0.000, 0.500, 0.312, 0.125, 0.500, 0.375, 0.250, 0.500, 0.438, 0.375,
1291-
0.500, 0.500, 0.500, 0.500, 0.562, 0.625, 0.500, 0.625, 0.750, 0.500,
1292-
0.688, 0.875, 0.500, 0.750, 1.000, 0.625, 0.312, 0.000, 0.625, 0.375,
1293-
0.125, 0.625, 0.438, 0.250, 0.625, 0.500, 0.375, 0.625, 0.562, 0.500,
1294-
0.625, 0.625, 0.625, 0.625, 0.688, 0.750, 0.625, 0.750, 0.875, 0.625,
1295-
0.812, 1.000, 0.750, 0.375, 0.000, 0.750, 0.438, 0.125, 0.750, 0.500,
1296-
0.250, 0.750, 0.562, 0.375, 0.750, 0.625, 0.500, 0.750, 0.688, 0.625,
1297-
0.750, 0.750, 0.750, 0.750, 0.812, 0.875, 0.750, 0.875, 1.000, 0.875,
1298-
0.438, 0.000, 0.875, 0.500, 0.125, 0.875, 0.562, 0.250, 0.875, 0.625,
1299-
0.375, 0.875, 0.688, 0.500, 0.875, 0.750, 0.625, 0.875, 0.812, 0.750,
1300-
0.875, 0.875, 0.875, 0.875, 0.938, 1.000, 1.000, 0.500, 0.000, 1.000,
1301-
0.562, 0.125, 1.000, 0.625, 0.250, 1.000, 0.688, 0.375, 1.000, 0.750,
1302-
0.500, 1.000, 0.812, 0.625, 1.000, 0.875, 0.750, 1.000, 0.938, 0.875,
1303-
1.000, 1.000, 1.000,
1304-
]).reshape((9, 9, 3))
1278+
[0.0, 0.0, 0.0, 0.0, 0.5, 1.0,
1279+
1.0, 0.5, 0.0, 1.0, 1.0, 1.0,
1280+
]).reshape((2, 2, 3))
13051281

13061282
cmaps = {
13071283
"BiPeak": SegmentedBivarColormap(

lib/matplotlib/colors.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555

5656
import matplotlib as mpl
5757
import numpy as np
58-
from matplotlib import _api, _cm, cbook, scale, _image
58+
from matplotlib import _api, _cm, cbook, scale
5959
from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
6060

6161

@@ -2212,16 +2212,37 @@ def __init__(self, patch, N=256, shape='square', origin=(0, 0),
22122212
super().__init__(N, N, shape, origin, name=name)
22132213

22142214
def _init(self):
2215+
# Perform bilinear interpolation
2216+
22152217
s = self.patch.shape
2216-
_patch = np.empty((s[0], s[1], 4))
2217-
_patch[:, :, :3] = self.patch
2218-
_patch[:, :, 3] = 1
2219-
transform = mpl.transforms.Affine2D().translate(-0.5, -0.5)\
2220-
.scale(self.N / (s[1] - 1), self.N / (s[0] - 1))
2221-
self._lut = np.empty((self.N, self.N, 4))
2222-
2223-
_image.resample(_patch, self._lut, transform, _image.BILINEAR,
2224-
resample=False, alpha=1)
2218+
2219+
# Indices (whole and fraction) of the new grid points
2220+
row = np.linspace(0, s[0] - 1, self.N)[:, np.newaxis]
2221+
col = np.linspace(0, s[1] - 1, self.N)[np.newaxis, :]
2222+
left = row.astype(int) # floor not needed because all values are nonnegative
2223+
top = col.astype(int) # floor not needed because all values are nonnegative
2224+
row_frac = (row - left)[:, :, np.newaxis]
2225+
col_frac = (col - top)[:, :, np.newaxis]
2226+
2227+
# Indices of the next edges, clipping where needed
2228+
right = np.clip(left + 1, 0, s[0] - 1)
2229+
bottom = np.clip(top + 1, 0, s[1] - 1)
2230+
2231+
# Values at the corners
2232+
tl = self.patch[left, top, :]
2233+
tr = self.patch[right, top, :]
2234+
bl = self.patch[left, bottom, :]
2235+
br = self.patch[right, bottom, :]
2236+
2237+
# Interpolate between the corners
2238+
lut = (tl * (1 - row_frac) * (1 - col_frac) +
2239+
tr * row_frac * (1 - col_frac) +
2240+
bl * (1 - row_frac) * col_frac +
2241+
br * row_frac * col_frac)
2242+
2243+
# Add the alpha channel
2244+
self._lut = np.concatenate([lut, np.ones((self.N, self.N, 1))], axis=2)
2245+
22252246
self._isinit = True
22262247

22272248

-73 Bytes
Loading

lib/matplotlib/tests/test_colors.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2132,30 +2132,30 @@ def test_colorizer_multinorm_implicit():
21322132

21332133
# test call with two single values
21342134
data = [0.1, 0.2]
2135-
res = (0.10009765625, 0.1510859375, 0.20166015625, 1.0)
2135+
res = (0.098039, 0.149020, 0.2, 1.0)
21362136
assert_array_almost_equal(ca.to_rgba(data), res)
21372137

21382138
# test call with two 1d arrays
21392139
data = [[0.1, 0.2], [0.3, 0.4]]
2140-
res = [[0.10009766, 0.19998877, 0.29931641, 1.],
2141-
[0.20166016, 0.30098633, 0.40087891, 1.]]
2140+
res = [[0.09803922, 0.19803922, 0.29803922, 1.],
2141+
[0.2, 0.3, 0.4, 1.]]
21422142
assert_array_almost_equal(ca.to_rgba(data), res)
21432143

21442144
# test call with two 2d arrays
21452145
data = [np.linspace(0, 1, 12).reshape(3, 4),
21462146
np.linspace(1, 0, 12).reshape(3, 4)]
2147-
res = np.array([[[0.00244141, 0.50048437, 0.99853516, 1.],
2148-
[0.09228516, 0.50048437, 0.90869141, 1.],
2149-
[0.18212891, 0.50048437, 0.81884766, 1.],
2150-
[0.27197266, 0.50048437, 0.72900391, 1.]],
2151-
[[0.36572266, 0.50048437, 0.63525391, 1.],
2152-
[0.45556641, 0.50048438, 0.54541016, 1.],
2153-
[0.54541016, 0.50048438, 0.45556641, 1.],
2154-
[0.63525391, 0.50048437, 0.36572266, 1.]],
2155-
[[0.72900391, 0.50048437, 0.27197266, 1.],
2156-
[0.81884766, 0.50048437, 0.18212891, 1.],
2157-
[0.90869141, 0.50048437, 0.09228516, 1.],
2158-
[0.99853516, 0.50048437, 0.00244141, 1.]]])
2147+
res = np.array([[[0., 0.5, 1., 1.],
2148+
[0.09019608, 0.5, 0.90980392, 1.],
2149+
[0.18039216, 0.5, 0.81960784, 1.],
2150+
[0.27058824, 0.5, 0.72941176, 1.]],
2151+
[[0.36470588, 0.5, 0.63529412, 1.],
2152+
[0.45490196, 0.5, 0.54509804, 1.],
2153+
[0.54509804, 0.5, 0.45490196, 1.],
2154+
[0.63529412, 0.5, 0.36470588, 1.]],
2155+
[[0.72941176, 0.5, 0.27058824, 1.],
2156+
[0.81960784, 0.5, 0.18039216, 1.],
2157+
[0.90980392, 0.5, 0.09019608, 1.],
2158+
[1., 0.5, 0., 1.]]])
21592159
assert_array_almost_equal(ca.to_rgba(data), res)
21602160

21612161
with pytest.raises(ValueError, match=("This MultiNorm has 2 components, "
@@ -2196,7 +2196,7 @@ def test_colorizer_multinorm_explicit():
21962196

21972197
# test call with two single values
21982198
data = [0.1, 0.2]
2199-
res = (0.100098, 0.375492, 0.650879, 1.)
2199+
res = (0.098039, 0.374510, 0.65098, 1.)
22002200
assert_array_almost_equal(ca.to_rgba(data), res)
22012201

22022202

lib/matplotlib/tests/test_multivariate_colormaps.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,26 @@ def test_multivar_resample():
212212

213213
def test_bivar_cmap_call_tuple():
214214
cmap = mpl.bivar_colormaps['BiOrangeBlue']
215-
assert_allclose(cmap((1.0, 1.0)), (1, 1, 1, 1), atol=0.01)
216-
assert_allclose(cmap((0.0, 0.0)), (0, 0, 0, 1), atol=0.1)
217-
assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1), atol=0.1)
215+
assert_allclose(cmap((1.0, 1.0)), (1, 1, 1, 1))
216+
assert_allclose(cmap((0.0, 0.0)), (0, 0, 0, 1))
217+
assert_allclose(cmap((0.2, 0.8)), (0.2, 0.5, 0.8, 1))
218+
assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1))
219+
220+
221+
def test_bivar_cmap_lut_smooth():
222+
cmap = mpl.bivar_colormaps['BiOrangeBlue']
223+
224+
assert_allclose(cmap.lut[:, 0, 0], np.linspace(0, 1, 256))
225+
assert_allclose(cmap.lut[:, 255, 0], np.linspace(0, 1, 256))
226+
assert_allclose(cmap.lut[:, 0, 1], np.linspace(0, 0.5, 256))
227+
assert_allclose(cmap.lut[:, 153, 1], np.linspace(0.3, 0.8, 256))
228+
assert_allclose(cmap.lut[:, 255, 1], np.linspace(0.5, 1, 256))
229+
230+
assert_allclose(cmap.lut[0, :, 1], np.linspace(0, 0.5, 256))
231+
assert_allclose(cmap.lut[102, :, 1], np.linspace(0.2, 0.7, 256))
232+
assert_allclose(cmap.lut[255, :, 1], np.linspace(0.5, 1, 256))
233+
assert_allclose(cmap.lut[0, :, 2], np.linspace(0, 1, 256))
234+
assert_allclose(cmap.lut[255, :, 2], np.linspace(0, 1, 256))
218235

219236

220237
def test_bivar_cmap_call():
@@ -312,20 +329,36 @@ def test_bivar_cmap_call():
312329
match="only implemented for use with with floats"):
313330
cs = cmap([(0, 5, 9, 0, 0, 9), (0, 0, 0, 5, 11, 11)])
314331

315-
# test origin
316-
cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(0.5, 0.5))
317-
assert_allclose(cmap[0](0.5),
318-
(0.50244140625, 0.5024222412109375, 0.50244140625, 1))
319-
assert_allclose(cmap[1](0.5),
320-
(0.50244140625, 0.5024222412109375, 0.50244140625, 1))
321-
cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(1, 1))
322-
assert_allclose(cmap[0](1.),
323-
(0.99853515625, 0.9985467529296875, 0.99853515625, 1.0))
324-
assert_allclose(cmap[1](1.),
325-
(0.99853515625, 0.9985467529296875, 0.99853515625, 1.0))
332+
333+
def test_bivar_cmap_1d_origin():
334+
"""
335+
Test getting 1D colormaps with different origins
336+
"""
337+
cmap0 = mpl.bivar_colormaps['BiOrangeBlue']
338+
assert_allclose(cmap0[0].colors[:, 0], np.linspace(0, 1, 256))
339+
assert_allclose(cmap0[0].colors[:, 1], np.linspace(0, 0.5, 256))
340+
assert_allclose(cmap0[0].colors[:, 2], 0)
341+
assert_allclose(cmap0[1].colors[:, 0], 0)
342+
assert_allclose(cmap0[1].colors[:, 1], np.linspace(0, 0.5, 256))
343+
assert_allclose(cmap0[1].colors[:, 2], np.linspace(0, 1, 256))
344+
345+
cmap1 = cmap0.with_extremes(origin=(0, 1))
346+
assert_allclose(cmap1[0].colors[:, 0], np.linspace(0, 1, 256))
347+
assert_allclose(cmap1[0].colors[:, 1], np.linspace(0.5, 1, 256))
348+
assert_allclose(cmap1[0].colors[:, 2], 1)
349+
assert_allclose(cmap1[1].colors, cmap0[1].colors)
350+
351+
cmap2 = cmap0.with_extremes(origin=(0.2, 0.4))
352+
assert_allclose(cmap2[0].colors[:, 0], np.linspace(0, 1, 256))
353+
assert_allclose(cmap2[0].colors[:, 1], np.linspace(0.2, 0.7, 256))
354+
assert_allclose(cmap2[0].colors[:, 2], 0.4)
355+
assert_allclose(cmap2[1].colors[:, 0], 0.2)
356+
assert_allclose(cmap2[1].colors[:, 1], np.linspace(0.1, 0.6, 256))
357+
assert_allclose(cmap2[1].colors[:, 2], np.linspace(0, 1, 256))
358+
326359
with pytest.raises(KeyError,
327360
match="only 0 or 1 are valid keys"):
328-
cs = cmap[2]
361+
cs = cmap0[2]
329362

330363

331364
def test_bivar_getitem():
@@ -433,22 +466,18 @@ def test_bivar_cmap_from_image():
433466

434467

435468
def test_bivar_resample():
436-
cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, 2))
437-
assert_allclose(cmap((0.25, 0.25)), (0, 0, 0, 1), atol=1e-2)
438-
439-
cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, 2))
440-
assert_allclose(cmap((0.25, 0.25)), (1., 0.5, 0., 1.), atol=1e-2)
469+
cmap = mpl.bivar_colormaps['BiOrangeBlue']
441470

442-
cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, -2))
443-
assert_allclose(cmap((0.25, 0.25)), (0., 0.5, 1., 1.), atol=1e-2)
471+
assert_allclose(cmap.resampled((2, 2))((0.25, 0.25)), (0, 0, 0, 1))
472+
assert_allclose(cmap.resampled((-2, 2))((0.25, 0.25)), (1., 0.5, 0., 1.))
473+
assert_allclose(cmap.resampled((2, -2))((0.25, 0.25)), (0., 0.5, 1., 1.))
474+
assert_allclose(cmap.resampled((-2, -2))((0.25, 0.25)), (1, 1, 1, 1))
444475

445-
cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, -2))
446-
assert_allclose(cmap((0.25, 0.25)), (1, 1, 1, 1), atol=1e-2)
476+
assert_allclose(cmap((0.8, 0.4)), (0.8, 0.6, 0.4, 1.))
477+
assert_allclose(cmap.reversed()((1 - 0.8, 1 - 0.4)), (0.8, 0.6, 0.4, 1.))
447478

448-
cmap = mpl.bivar_colormaps['BiOrangeBlue'].reversed()
449-
assert_allclose(cmap((0.25, 0.25)), (0.748535, 0.748547, 0.748535, 1.), atol=1e-2)
450-
cmap = mpl.bivar_colormaps['BiOrangeBlue'].transposed()
451-
assert_allclose(cmap((0.25, 0.25)), (0.252441, 0.252422, 0.252441, 1.), atol=1e-2)
479+
assert_allclose(cmap((0.6, 0.2)), (0.6, 0.4, 0.2, 1.))
480+
assert_allclose(cmap.transposed()((0.2, 0.6)), (0.6, 0.4, 0.2, 1.))
452481

453482
with pytest.raises(ValueError, match="lutshape must be of length"):
454483
cmap = cmap.resampled(4)

0 commit comments

Comments
 (0)