From 9b4ec6da74eca752bf2cabdc69c9cab6c64bd21a Mon Sep 17 00:00:00 2001 From: "Albert Y. Shih" Date: Mon, 8 Dec 2025 12:26:07 -0500 Subject: [PATCH 1/4] Fix bilinear interpolation for SegmentedBivarColormap --- lib/matplotlib/colors.py | 41 ++++++++++---- .../bivariate_cmap_shapes.png | Bin 5157 -> 5084 bytes lib/matplotlib/tests/test_colors.py | 32 +++++------ .../tests/test_multivariate_colormaps.py | 53 ++++++++++++------ 4 files changed, 84 insertions(+), 42 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 07cbe4a79cb0..97c4fa9c3796 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -55,7 +55,7 @@ import matplotlib as mpl import numpy as np -from matplotlib import _api, _cm, cbook, scale, _image +from matplotlib import _api, _cm, cbook, scale from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS @@ -2211,16 +2211,37 @@ def __init__(self, patch, N=256, shape='square', origin=(0, 0), super().__init__(N, N, shape, origin, name=name) def _init(self): + # Perform bilinear interpolation + s = self.patch.shape - _patch = np.empty((s[0], s[1], 4)) - _patch[:, :, :3] = self.patch - _patch[:, :, 3] = 1 - transform = mpl.transforms.Affine2D().translate(-0.5, -0.5)\ - .scale(self.N / (s[1] - 1), self.N / (s[0] - 1)) - self._lut = np.empty((self.N, self.N, 4)) - - _image.resample(_patch, self._lut, transform, _image.BILINEAR, - resample=False, alpha=1) + + # Indices (whole and fraction) of the new grid points + row = np.linspace(0, s[0] - 1, self.N)[:, np.newaxis] + col = np.linspace(0, s[1] - 1, self.N)[np.newaxis, :] + left = np.floor(row).astype(int) + top = np.floor(col).astype(int) + row_frac = (row - left)[:, :, np.newaxis] + col_frac = (col - top)[:, :, np.newaxis] + + # Indices of the next edges, clipping where needed + right = np.clip(left + 1, 0, s[0] - 1) + bottom = np.clip(top + 1, 0, s[1] - 1) + + # Values at the corners + tl = self.patch[left, top, :] + tr = self.patch[right, top, :] + bl = self.patch[left, bottom, :] + br = self.patch[right, bottom, :] + + # Interpolate between the corners + lut = (tl * (1 - row_frac) * (1 - col_frac) + + tr * row_frac * (1 - col_frac) + + bl * (1 - row_frac) * col_frac + + br * row_frac * col_frac) + + # Add the alpha channel + self._lut = np.concatenate([lut, np.ones((self.N, self.N, 1))], axis=2) + self._isinit = True diff --git a/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png b/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png index f22b446fc84b5c0675d96f50faece58312ecc7d1..5aba927bbcf18ebb928259b98b850e7e93b2adb3 100644 GIT binary patch literal 5084 zcmcIod0dm%nof(BS`|vE#SO54pduhU1wyEz7+GQ?in1zU6~Y!EKuDss6+spuDm%Df zXn+vID#Z{iTM-GH5C}^O2!TLAAPZ!@p`APPYo{~6_RgI@&Ue1^edj&vbDrmYPs({$ z$K9Xo{{#R4>;|88fC2yt^YZtfKmJJmM6tRd^2{aX^yQcf5rHwtYtaD!muoSRVG%K5 zA%2JA0-_Ni5#gqW#>R#x4E+P*jLl6<^nxlD5CDMkwXO8Q51_IT06^6o?C`Z)d?9B7^?W=aLGgW%U>u?Jm4kM) zN{>C@C@#b~Ebr32w`U&vT=?|UR|I%jeQc$L$y1YhQ}Il2QGc3tY#S{;5A1Y$+kM8D zGw}wdaQN<@PrJ38+3xW1Jw&uC4K4_ioqe(79H+`ZoWPurB*oEot≧>;A}+u;!-Z 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 Y^1Zo2jPr$AdB*_oX;+7;lUKj}2hyY`u>b%7 literal 5157 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 diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 4af0c84261b8..b1077cfa94ab 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -2127,30 +2127,30 @@ def test_colorizer_multinorm_implicit(): # test call with two single values data = [0.1, 0.2] - res = (0.10009765625, 0.1510859375, 0.20166015625, 1.0) + res = (0.098039, 0.149212, 0.2, 1.0) assert_array_almost_equal(ca.to_rgba(data), res) # test call with two 1d arrays data = [[0.1, 0.2], [0.3, 0.4]] - res = [[0.10009766, 0.19998877, 0.29931641, 1.], - [0.20166016, 0.30098633, 0.40087891, 1.]] + res = [[0.09803922, 0.19832211, 0.29803922, 1.], + [0.2, 0.29972, 0.4, 1.]] assert_array_almost_equal(ca.to_rgba(data), res) # test call with two 2d arrays data = [np.linspace(0, 1, 12).reshape(3, 4), np.linspace(1, 0, 12).reshape(3, 4)] - res = np.array([[[0.00244141, 0.50048437, 0.99853516, 1.], - [0.09228516, 0.50048437, 0.90869141, 1.], - [0.18212891, 0.50048437, 0.81884766, 1.], - [0.27197266, 0.50048437, 0.72900391, 1.]], - [[0.36572266, 0.50048437, 0.63525391, 1.], - [0.45556641, 0.50048438, 0.54541016, 1.], - [0.54541016, 0.50048438, 0.45556641, 1.], - [0.63525391, 0.50048437, 0.36572266, 1.]], - [[0.72900391, 0.50048437, 0.27197266, 1.], - [0.81884766, 0.50048437, 0.18212891, 1.], - [0.90869141, 0.50048437, 0.09228516, 1.], - [0.99853516, 0.50048437, 0.00244141, 1.]]]) + res = np.array([[[0., 0.5, 1., 1.], + [0.09019608, 0.5, 0.90980392, 1.], + [0.18039216, 0.5, 0.81960784, 1.], + [0.27058824, 0.5, 0.72941176, 1.]], + [[0.36470588, 0.5, 0.63529412, 1.], + [0.45490196, 0.5, 0.54509804, 1.], + [0.54509804, 0.5, 0.45490196, 1.], + [0.63529412, 0.5, 0.36470588, 1.]], + [[0.72941176, 0.5, 0.27058824, 1.], + [0.81960784, 0.5, 0.18039216, 1.], + [0.90980392, 0.5, 0.09019608, 1.], + [1., 0.5, 0., 1.]]]) assert_array_almost_equal(ca.to_rgba(data), res) with pytest.raises(ValueError, match=("This MultiNorm has 2 components, " @@ -2191,7 +2191,7 @@ def test_colorizer_multinorm_explicit(): # test call with two single values data = [0.1, 0.2] - res = (0.100098, 0.375492, 0.650879, 1.) + res = (0.098039, 0.374506, 0.65098, 1.) assert_array_almost_equal(ca.to_rgba(data), res) diff --git a/lib/matplotlib/tests/test_multivariate_colormaps.py b/lib/matplotlib/tests/test_multivariate_colormaps.py index 81a2e6adeb35..e474699b4989 100644 --- a/lib/matplotlib/tests/test_multivariate_colormaps.py +++ b/lib/matplotlib/tests/test_multivariate_colormaps.py @@ -212,9 +212,18 @@ def test_multivar_resample(): def test_bivar_cmap_call_tuple(): cmap = mpl.bivar_colormaps['BiOrangeBlue'] - assert_allclose(cmap((1.0, 1.0)), (1, 1, 1, 1), atol=0.01) - assert_allclose(cmap((0.0, 0.0)), (0, 0, 0, 1), atol=0.1) - assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1), atol=0.1) + assert_allclose(cmap((1.0, 1.0)), (1, 1, 1, 1)) + assert_allclose(cmap((0.0, 0.0)), (0, 0, 0, 1)) + assert_allclose(cmap((0.2, 0.8)), (0.2, 0.5, 0.8, 1)) + assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1)) + + +def test_bivar_cmap_lut_smooth(): + cmap = mpl.bivar_colormaps['BiOrangeBlue'] + assert_allclose(cmap.lut[:, 0, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap.lut[:, 0, 1], np.linspace(0, 0.5, 256), atol=1e-3) + assert_allclose(cmap.lut[0, :, 1], np.linspace(0, 0.5, 256), atol=1e-3) + assert_allclose(cmap.lut[0, :, 2], np.linspace(0, 1, 256)) def test_bivar_cmap_call(): @@ -312,17 +321,29 @@ def test_bivar_cmap_call(): match="only implemented for use with with floats"): cs = cmap([(0, 5, 9, 0, 0, 9), (0, 0, 0, 5, 11, 11)]) - # test origin + +def test_bivar_cmap_1d_origin(): + """ + Test getting 1D colormaps with different origins + """ + cmap = mpl.bivar_colormaps['BiOrangeBlue'] + assert_allclose(cmap[0](1.), (1., 0.5, 0., 1.)) + assert_allclose(cmap[1](1.), (0., 0.5, 1., 1.)) + + cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(0, 1)) + assert_allclose(cmap[0](1.), (1., 1., 1., 1.)) + assert_allclose(cmap[1](1.), (0., 0.5, 1., 1.)) + cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(0.5, 0.5)) assert_allclose(cmap[0](0.5), - (0.50244140625, 0.5024222412109375, 0.50244140625, 1)) + (0.5019607843137255, 0.5019453440984237, 0.5019607843137255, 1)) assert_allclose(cmap[1](0.5), - (0.50244140625, 0.5024222412109375, 0.50244140625, 1)) + (0.5019607843137255, 0.5019453440984237, 0.5019607843137255, 1)) + cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(1, 1)) - assert_allclose(cmap[0](1.), - (0.99853515625, 0.9985467529296875, 0.99853515625, 1.0)) - assert_allclose(cmap[1](1.), - (0.99853515625, 0.9985467529296875, 0.99853515625, 1.0)) + assert_allclose(cmap[0](1.), (1., 1., 1., 1.)) + assert_allclose(cmap[1](1.), (1., 1., 1., 1.)) + with pytest.raises(KeyError, match="only 0 or 1 are valid keys"): cs = cmap[2] @@ -434,21 +455,21 @@ def test_bivar_cmap_from_image(): def test_bivar_resample(): cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, 2)) - assert_allclose(cmap((0.25, 0.25)), (0, 0, 0, 1), atol=1e-2) + assert_allclose(cmap((0.25, 0.25)), (0, 0, 0, 1)) cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, 2)) - assert_allclose(cmap((0.25, 0.25)), (1., 0.5, 0., 1.), atol=1e-2) + assert_allclose(cmap((0.25, 0.25)), (1., 0.5, 0., 1.)) cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, -2)) - assert_allclose(cmap((0.25, 0.25)), (0., 0.5, 1., 1.), atol=1e-2) + assert_allclose(cmap((0.25, 0.25)), (0., 0.5, 1., 1.)) cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, -2)) - assert_allclose(cmap((0.25, 0.25)), (1, 1, 1, 1), atol=1e-2) + assert_allclose(cmap((0.25, 0.25)), (1, 1, 1, 1)) cmap = mpl.bivar_colormaps['BiOrangeBlue'].reversed() - assert_allclose(cmap((0.25, 0.25)), (0.748535, 0.748547, 0.748535, 1.), atol=1e-2) + assert_allclose(cmap((0.25, 0.25)), (0.74902, 0.74902, 0.74902, 1.), atol=1e-5) cmap = mpl.bivar_colormaps['BiOrangeBlue'].transposed() - assert_allclose(cmap((0.25, 0.25)), (0.252441, 0.252422, 0.252441, 1.), atol=1e-2) + assert_allclose(cmap((0.25, 0.25)), (0.25098, 0.25098, 0.25098, 1.), atol=1e-5) with pytest.raises(ValueError, match="lutshape must be of length"): cmap = cmap.resampled(4) From 4c66b4ecb090248b3a44f9cebc9cfe470920235c Mon Sep 17 00:00:00 2001 From: "Albert Y. Shih" Date: Tue, 9 Dec 2025 09:41:25 -0500 Subject: [PATCH 2/4] Increase precision and overhaul tests --- lib/matplotlib/_cm_bivar.py | 48 ++++++------ lib/matplotlib/tests/test_colors.py | 8 +- .../tests/test_multivariate_colormaps.py | 74 ++++++++++--------- 3 files changed, 69 insertions(+), 61 deletions(-) diff --git a/lib/matplotlib/_cm_bivar.py b/lib/matplotlib/_cm_bivar.py index 53c0d48d7d6c..59c311385c56 100644 --- a/lib/matplotlib/_cm_bivar.py +++ b/lib/matplotlib/_cm_bivar.py @@ -1276,30 +1276,30 @@ ]).reshape((65, 65, 3)) BiOrangeBlue = np.array( - [0.000, 0.000, 0.000, 0.000, 0.062, 0.125, 0.000, 0.125, 0.250, 0.000, - 0.188, 0.375, 0.000, 0.250, 0.500, 0.000, 0.312, 0.625, 0.000, 0.375, - 0.750, 0.000, 0.438, 0.875, 0.000, 0.500, 1.000, 0.125, 0.062, 0.000, - 0.125, 0.125, 0.125, 0.125, 0.188, 0.250, 0.125, 0.250, 0.375, 0.125, - 0.312, 0.500, 0.125, 0.375, 0.625, 0.125, 0.438, 0.750, 0.125, 0.500, - 0.875, 0.125, 0.562, 1.000, 0.250, 0.125, 0.000, 0.250, 0.188, 0.125, - 0.250, 0.250, 0.250, 0.250, 0.312, 0.375, 0.250, 0.375, 0.500, 0.250, - 0.438, 0.625, 0.250, 0.500, 0.750, 0.250, 0.562, 0.875, 0.250, 0.625, - 1.000, 0.375, 0.188, 0.000, 0.375, 0.250, 0.125, 0.375, 0.312, 0.250, - 0.375, 0.375, 0.375, 0.375, 0.438, 0.500, 0.375, 0.500, 0.625, 0.375, - 0.562, 0.750, 0.375, 0.625, 0.875, 0.375, 0.688, 1.000, 0.500, 0.250, - 0.000, 0.500, 0.312, 0.125, 0.500, 0.375, 0.250, 0.500, 0.438, 0.375, - 0.500, 0.500, 0.500, 0.500, 0.562, 0.625, 0.500, 0.625, 0.750, 0.500, - 0.688, 0.875, 0.500, 0.750, 1.000, 0.625, 0.312, 0.000, 0.625, 0.375, - 0.125, 0.625, 0.438, 0.250, 0.625, 0.500, 0.375, 0.625, 0.562, 0.500, - 0.625, 0.625, 0.625, 0.625, 0.688, 0.750, 0.625, 0.750, 0.875, 0.625, - 0.812, 1.000, 0.750, 0.375, 0.000, 0.750, 0.438, 0.125, 0.750, 0.500, - 0.250, 0.750, 0.562, 0.375, 0.750, 0.625, 0.500, 0.750, 0.688, 0.625, - 0.750, 0.750, 0.750, 0.750, 0.812, 0.875, 0.750, 0.875, 1.000, 0.875, - 0.438, 0.000, 0.875, 0.500, 0.125, 0.875, 0.562, 0.250, 0.875, 0.625, - 0.375, 0.875, 0.688, 0.500, 0.875, 0.750, 0.625, 0.875, 0.812, 0.750, - 0.875, 0.875, 0.875, 0.875, 0.938, 1.000, 1.000, 0.500, 0.000, 1.000, - 0.562, 0.125, 1.000, 0.625, 0.250, 1.000, 0.688, 0.375, 1.000, 0.750, - 0.500, 1.000, 0.812, 0.625, 1.000, 0.875, 0.750, 1.000, 0.938, 0.875, + [0.000, 0.000, 0.000, 0.000, 0.0625, 0.125, 0.000, 0.125, 0.250, 0.000, + 0.1875, 0.375, 0.000, 0.250, 0.500, 0.000, 0.3125, 0.625, 0.000, 0.375, + 0.750, 0.000, 0.4375, 0.875, 0.000, 0.500, 1.000, 0.125, 0.0625, 0.000, + 0.125, 0.125, 0.125, 0.125, 0.1875, 0.250, 0.125, 0.250, 0.375, 0.125, + 0.3125, 0.500, 0.125, 0.375, 0.625, 0.125, 0.4375, 0.750, 0.125, 0.500, + 0.875, 0.125, 0.5625, 1.000, 0.250, 0.125, 0.000, 0.250, 0.1875, 0.125, + 0.250, 0.250, 0.250, 0.250, 0.3125, 0.375, 0.250, 0.375, 0.500, 0.250, + 0.4375, 0.625, 0.250, 0.500, 0.750, 0.250, 0.5625, 0.875, 0.250, 0.625, + 1.000, 0.375, 0.1875, 0.000, 0.375, 0.250, 0.125, 0.375, 0.3125, 0.250, + 0.375, 0.375, 0.375, 0.375, 0.4375, 0.500, 0.375, 0.500, 0.625, 0.375, + 0.5625, 0.750, 0.375, 0.625, 0.875, 0.375, 0.6875, 1.000, 0.500, 0.250, + 0.000, 0.500, 0.3125, 0.125, 0.500, 0.375, 0.250, 0.500, 0.4375, 0.375, + 0.500, 0.500, 0.500, 0.500, 0.5625, 0.625, 0.500, 0.625, 0.750, 0.500, + 0.6875, 0.875, 0.500, 0.750, 1.000, 0.625, 0.3125, 0.000, 0.625, 0.375, + 0.125, 0.625, 0.4375, 0.250, 0.625, 0.500, 0.375, 0.625, 0.5625, 0.500, + 0.625, 0.625, 0.625, 0.625, 0.6875, 0.750, 0.625, 0.750, 0.875, 0.625, + 0.8125, 1.000, 0.750, 0.375, 0.000, 0.750, 0.4375, 0.125, 0.750, 0.500, + 0.250, 0.750, 0.5625, 0.375, 0.750, 0.625, 0.500, 0.750, 0.6875, 0.625, + 0.750, 0.750, 0.750, 0.750, 0.8125, 0.875, 0.750, 0.875, 1.000, 0.875, + 0.4375, 0.000, 0.875, 0.500, 0.125, 0.875, 0.5625, 0.250, 0.875, 0.625, + 0.375, 0.875, 0.6875, 0.500, 0.875, 0.750, 0.625, 0.875, 0.8125, 0.750, + 0.875, 0.875, 0.875, 0.875, 0.9375, 1.000, 1.000, 0.500, 0.000, 1.000, + 0.5625, 0.125, 1.000, 0.625, 0.250, 1.000, 0.6875, 0.375, 1.000, 0.750, + 0.500, 1.000, 0.8125, 0.625, 1.000, 0.875, 0.750, 1.000, 0.9375, 0.875, 1.000, 1.000, 1.000, ]).reshape((9, 9, 3)) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index b1077cfa94ab..fe6b5d309e5b 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -2127,13 +2127,13 @@ def test_colorizer_multinorm_implicit(): # test call with two single values data = [0.1, 0.2] - res = (0.098039, 0.149212, 0.2, 1.0) + res = (0.098039, 0.149020, 0.2, 1.0) assert_array_almost_equal(ca.to_rgba(data), res) # test call with two 1d arrays data = [[0.1, 0.2], [0.3, 0.4]] - res = [[0.09803922, 0.19832211, 0.29803922, 1.], - [0.2, 0.29972, 0.4, 1.]] + res = [[0.09803922, 0.19803922, 0.29803922, 1.], + [0.2, 0.3, 0.4, 1.]] assert_array_almost_equal(ca.to_rgba(data), res) # test call with two 2d arrays @@ -2191,7 +2191,7 @@ def test_colorizer_multinorm_explicit(): # test call with two single values data = [0.1, 0.2] - res = (0.098039, 0.374506, 0.65098, 1.) + res = (0.098039, 0.374510, 0.65098, 1.) assert_array_almost_equal(ca.to_rgba(data), res) diff --git a/lib/matplotlib/tests/test_multivariate_colormaps.py b/lib/matplotlib/tests/test_multivariate_colormaps.py index e474699b4989..592058212a24 100644 --- a/lib/matplotlib/tests/test_multivariate_colormaps.py +++ b/lib/matplotlib/tests/test_multivariate_colormaps.py @@ -220,10 +220,18 @@ def test_bivar_cmap_call_tuple(): def test_bivar_cmap_lut_smooth(): cmap = mpl.bivar_colormaps['BiOrangeBlue'] + assert_allclose(cmap.lut[:, 0, 0], np.linspace(0, 1, 256)) - assert_allclose(cmap.lut[:, 0, 1], np.linspace(0, 0.5, 256), atol=1e-3) - assert_allclose(cmap.lut[0, :, 1], np.linspace(0, 0.5, 256), atol=1e-3) + assert_allclose(cmap.lut[:, 255, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap.lut[:, 0, 1], np.linspace(0, 0.5, 256)) + assert_allclose(cmap.lut[:, 153, 1], np.linspace(0.3, 0.8, 256)) + assert_allclose(cmap.lut[:, 255, 1], np.linspace(0.5, 1, 256)) + + assert_allclose(cmap.lut[0, :, 1], np.linspace(0, 0.5, 256)) + assert_allclose(cmap.lut[102, :, 1], np.linspace(0.2, 0.7, 256)) + assert_allclose(cmap.lut[255, :, 1], np.linspace(0.5, 1, 256)) assert_allclose(cmap.lut[0, :, 2], np.linspace(0, 1, 256)) + assert_allclose(cmap.lut[255, :, 2], np.linspace(0, 1, 256)) def test_bivar_cmap_call(): @@ -326,27 +334,31 @@ def test_bivar_cmap_1d_origin(): """ Test getting 1D colormaps with different origins """ - cmap = mpl.bivar_colormaps['BiOrangeBlue'] - assert_allclose(cmap[0](1.), (1., 0.5, 0., 1.)) - assert_allclose(cmap[1](1.), (0., 0.5, 1., 1.)) - - cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(0, 1)) - assert_allclose(cmap[0](1.), (1., 1., 1., 1.)) - assert_allclose(cmap[1](1.), (0., 0.5, 1., 1.)) - - cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(0.5, 0.5)) - assert_allclose(cmap[0](0.5), - (0.5019607843137255, 0.5019453440984237, 0.5019607843137255, 1)) - assert_allclose(cmap[1](0.5), - (0.5019607843137255, 0.5019453440984237, 0.5019607843137255, 1)) - - cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(1, 1)) - assert_allclose(cmap[0](1.), (1., 1., 1., 1.)) - assert_allclose(cmap[1](1.), (1., 1., 1., 1.)) + cmap0 = mpl.bivar_colormaps['BiOrangeBlue'] + assert_allclose(cmap0[0].colors[:, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap0[0].colors[:, 1], np.linspace(0, 0.5, 256)) + assert_allclose(cmap0[0].colors[:, 2], 0) + assert_allclose(cmap0[1].colors[:, 0], 0) + assert_allclose(cmap0[1].colors[:, 1], np.linspace(0, 0.5, 256)) + assert_allclose(cmap0[1].colors[:, 2], np.linspace(0, 1, 256)) + + cmap1 = cmap0.with_extremes(origin=(0, 1)) + assert_allclose(cmap1[0].colors[:, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap1[0].colors[:, 1], np.linspace(0.5, 1, 256)) + assert_allclose(cmap1[0].colors[:, 2], 1) + assert_allclose(cmap1[1].colors, cmap0[1].colors) + + cmap2 = cmap0.with_extremes(origin=(0.2, 0.4)) + assert_allclose(cmap2[0].colors[:, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap2[0].colors[:, 1], np.linspace(0.2, 0.7, 256)) + assert_allclose(cmap2[0].colors[:, 2], 0.4) + assert_allclose(cmap2[1].colors[:, 0], 0.2) + assert_allclose(cmap2[1].colors[:, 1], np.linspace(0.1, 0.6, 256)) + assert_allclose(cmap2[1].colors[:, 2], np.linspace(0, 1, 256)) with pytest.raises(KeyError, match="only 0 or 1 are valid keys"): - cs = cmap[2] + cs = cmap0[2] def test_bivar_getitem(): @@ -454,22 +466,18 @@ def test_bivar_cmap_from_image(): def test_bivar_resample(): - cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, 2)) - assert_allclose(cmap((0.25, 0.25)), (0, 0, 0, 1)) - - cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, 2)) - assert_allclose(cmap((0.25, 0.25)), (1., 0.5, 0., 1.)) + cmap = mpl.bivar_colormaps['BiOrangeBlue'] - cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, -2)) - assert_allclose(cmap((0.25, 0.25)), (0., 0.5, 1., 1.)) + assert_allclose(cmap.resampled((2, 2))((0.25, 0.25)), (0, 0, 0, 1)) + assert_allclose(cmap.resampled((-2, 2))((0.25, 0.25)), (1., 0.5, 0., 1.)) + assert_allclose(cmap.resampled((2, -2))((0.25, 0.25)), (0., 0.5, 1., 1.)) + assert_allclose(cmap.resampled((-2, -2))((0.25, 0.25)), (1, 1, 1, 1)) - cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, -2)) - assert_allclose(cmap((0.25, 0.25)), (1, 1, 1, 1)) + assert_allclose(cmap((0.8, 0.4)), (0.8, 0.6, 0.4, 1.)) + assert_allclose(cmap.reversed()((1 - 0.8, 1 - 0.4)), (0.8, 0.6, 0.4, 1.)) - cmap = mpl.bivar_colormaps['BiOrangeBlue'].reversed() - assert_allclose(cmap((0.25, 0.25)), (0.74902, 0.74902, 0.74902, 1.), atol=1e-5) - cmap = mpl.bivar_colormaps['BiOrangeBlue'].transposed() - assert_allclose(cmap((0.25, 0.25)), (0.25098, 0.25098, 0.25098, 1.), atol=1e-5) + assert_allclose(cmap((0.6, 0.2)), (0.6, 0.4, 0.2, 1.)) + assert_allclose(cmap.transposed()((0.2, 0.6)), (0.6, 0.4, 0.2, 1.)) with pytest.raises(ValueError, match="lutshape must be of length"): cmap = cmap.resampled(4) From e954ebeea4f6785582c772f22fa286b14eac66d7 Mon Sep 17 00:00:00 2001 From: "Albert Y. Shih" Date: Tue, 9 Dec 2025 12:28:22 -0500 Subject: [PATCH 3/4] Address comment and add changelog entry --- doc/api/next_api_changes/behavior/30824-AYS.rst | 6 ++++++ lib/matplotlib/colors.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/30824-AYS.rst diff --git a/doc/api/next_api_changes/behavior/30824-AYS.rst b/doc/api/next_api_changes/behavior/30824-AYS.rst new file mode 100644 index 000000000000..a190bd537126 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30824-AYS.rst @@ -0,0 +1,6 @@ +Bivariate colormaps now fully span the intended range of colors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Bivariate colormaps generated by ``SegmentedBivarColormap`` (e.g., ``BiOrangeBlue``) +from a set of input colors now fully span that range of colors. There had been a bug +with the numerical interpolation such that the colormap did not actually include the +first or last colors. diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 97c4fa9c3796..3fc8cf22312a 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -2218,8 +2218,8 @@ def _init(self): # Indices (whole and fraction) of the new grid points row = np.linspace(0, s[0] - 1, self.N)[:, np.newaxis] col = np.linspace(0, s[1] - 1, self.N)[np.newaxis, :] - left = np.floor(row).astype(int) - top = np.floor(col).astype(int) + left = row.astype(int) # floor not needed because all values are nonnegative + top = col.astype(int) # floor not needed because all values are nonnegative row_frac = (row - left)[:, :, np.newaxis] col_frac = (col - top)[:, :, np.newaxis] From 5a56a90bfff4020c63ead4df68c50aae95b6db77 Mon Sep 17 00:00:00 2001 From: "Albert Y. Shih" Date: Tue, 9 Dec 2025 12:28:59 -0500 Subject: [PATCH 4/4] Simplify BiOrangeBlue definition --- lib/matplotlib/_cm_bivar.py | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/lib/matplotlib/_cm_bivar.py b/lib/matplotlib/_cm_bivar.py index 59c311385c56..688e243accda 100644 --- a/lib/matplotlib/_cm_bivar.py +++ b/lib/matplotlib/_cm_bivar.py @@ -1,9 +1,8 @@ -# auto-generated by https://github.com/trygvrad/multivariate_colormaps -# date: 2024-05-24 - import numpy as np from matplotlib.colors import SegmentedBivarColormap +# auto-generated by https://github.com/trygvrad/multivariate_colormaps +# date: 2024-05-24 BiPeak = np.array( [0.000, 0.674, 0.931, 0.000, 0.680, 0.922, 0.000, 0.685, 0.914, 0.000, 0.691, 0.906, 0.000, 0.696, 0.898, 0.000, 0.701, 0.890, 0.000, 0.706, @@ -1276,32 +1275,9 @@ ]).reshape((65, 65, 3)) BiOrangeBlue = np.array( - [0.000, 0.000, 0.000, 0.000, 0.0625, 0.125, 0.000, 0.125, 0.250, 0.000, - 0.1875, 0.375, 0.000, 0.250, 0.500, 0.000, 0.3125, 0.625, 0.000, 0.375, - 0.750, 0.000, 0.4375, 0.875, 0.000, 0.500, 1.000, 0.125, 0.0625, 0.000, - 0.125, 0.125, 0.125, 0.125, 0.1875, 0.250, 0.125, 0.250, 0.375, 0.125, - 0.3125, 0.500, 0.125, 0.375, 0.625, 0.125, 0.4375, 0.750, 0.125, 0.500, - 0.875, 0.125, 0.5625, 1.000, 0.250, 0.125, 0.000, 0.250, 0.1875, 0.125, - 0.250, 0.250, 0.250, 0.250, 0.3125, 0.375, 0.250, 0.375, 0.500, 0.250, - 0.4375, 0.625, 0.250, 0.500, 0.750, 0.250, 0.5625, 0.875, 0.250, 0.625, - 1.000, 0.375, 0.1875, 0.000, 0.375, 0.250, 0.125, 0.375, 0.3125, 0.250, - 0.375, 0.375, 0.375, 0.375, 0.4375, 0.500, 0.375, 0.500, 0.625, 0.375, - 0.5625, 0.750, 0.375, 0.625, 0.875, 0.375, 0.6875, 1.000, 0.500, 0.250, - 0.000, 0.500, 0.3125, 0.125, 0.500, 0.375, 0.250, 0.500, 0.4375, 0.375, - 0.500, 0.500, 0.500, 0.500, 0.5625, 0.625, 0.500, 0.625, 0.750, 0.500, - 0.6875, 0.875, 0.500, 0.750, 1.000, 0.625, 0.3125, 0.000, 0.625, 0.375, - 0.125, 0.625, 0.4375, 0.250, 0.625, 0.500, 0.375, 0.625, 0.5625, 0.500, - 0.625, 0.625, 0.625, 0.625, 0.6875, 0.750, 0.625, 0.750, 0.875, 0.625, - 0.8125, 1.000, 0.750, 0.375, 0.000, 0.750, 0.4375, 0.125, 0.750, 0.500, - 0.250, 0.750, 0.5625, 0.375, 0.750, 0.625, 0.500, 0.750, 0.6875, 0.625, - 0.750, 0.750, 0.750, 0.750, 0.8125, 0.875, 0.750, 0.875, 1.000, 0.875, - 0.4375, 0.000, 0.875, 0.500, 0.125, 0.875, 0.5625, 0.250, 0.875, 0.625, - 0.375, 0.875, 0.6875, 0.500, 0.875, 0.750, 0.625, 0.875, 0.8125, 0.750, - 0.875, 0.875, 0.875, 0.875, 0.9375, 1.000, 1.000, 0.500, 0.000, 1.000, - 0.5625, 0.125, 1.000, 0.625, 0.250, 1.000, 0.6875, 0.375, 1.000, 0.750, - 0.500, 1.000, 0.8125, 0.625, 1.000, 0.875, 0.750, 1.000, 0.9375, 0.875, - 1.000, 1.000, 1.000, - ]).reshape((9, 9, 3)) + [0.0, 0.0, 0.0, 0.0, 0.5, 1.0, + 1.0, 0.5, 0.0, 1.0, 1.0, 1.0, + ]).reshape((2, 2, 3)) cmaps = { "BiPeak": SegmentedBivarColormap(