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/_cm_bivar.py b/lib/matplotlib/_cm_bivar.py index 53c0d48d7d6c..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.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, - 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( diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 07cbe4a79cb0..3fc8cf22312a 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 = 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] + + # 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 f22b446fc84b..5aba927bbcf1 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png and b/lib/matplotlib/tests/baseline_images/test_multivariate_colormaps/bivariate_cmap_shapes.png differ diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 4af0c84261b8..fe6b5d309e5b 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.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.10009766, 0.19998877, 0.29931641, 1.], - [0.20166016, 0.30098633, 0.40087891, 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 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.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 81a2e6adeb35..592058212a24 100644 --- a/lib/matplotlib/tests/test_multivariate_colormaps.py +++ b/lib/matplotlib/tests/test_multivariate_colormaps.py @@ -212,9 +212,26 @@ 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[:, 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(): @@ -312,20 +329,36 @@ 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 - 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)) - assert_allclose(cmap[1](0.5), - (0.50244140625, 0.5024222412109375, 0.50244140625, 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)) + +def test_bivar_cmap_1d_origin(): + """ + Test getting 1D colormaps with different origins + """ + 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(): @@ -433,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), atol=1e-2) - - cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, 2)) - assert_allclose(cmap((0.25, 0.25)), (1., 0.5, 0., 1.), atol=1e-2) + 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.), atol=1e-2) + 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), atol=1e-2) + 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.748535, 0.748547, 0.748535, 1.), atol=1e-2) - 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.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)