🌐 AI搜索 & 代理 主页
Skip to content

Conversation

@ayshih
Copy link
Contributor

@ayshih ayshih commented Dec 8, 2025

PR summary

The lookup table generated by SegmentedBivarColormap is slightly inaccurate. Most notably, the darkest color in the lookup table is not as dark as the darkest color in the input, and the same goes for the brightest color. This has been glossed over in the tests due to the liberal use of atol.

>>> from matplotlib.cm import bivar_cmaps
>>> cmap = bivar_cmaps['BiOrangeBlue']
>>> print(cmap.patch[0, 0, :])
[0. 0. 0.]
>>> print(cmap.patch[-1, 0, :])
[1.  0.5 0. ]
>>> print(cmap.patch[-1, -1, :])
[1. 1. 1.]
>>> print(cmap.lut[0, 0, :])
[0.00244141 0.00242224 0.00244141 1.        ]
>>> print(cmap.lut[-1, 0, :])
[0.99853516 0.50048437 0.00244141 1.        ]
>>> print(cmap.lut[-1, -1, :])
[0.99853516 0.99854675 0.99853516 1.        ]

SegmentedBivarColormap uses the image-plotting resampler (Agg) to perform bilinear interpolation, and these inaccuracies result from a bug in the transform definition plus a second bug that would be fixed by #30184. However, just fixing these two bugs is not a good solution. The Agg resampler – which is intended for producing good-enough images for visual display – is problematic because it divides each pixel into 256 subpixels, which means it internally calculates 256 * M - 1 intermediate color candidates between the end colors, but the color table wants 254 intermediate colors between the end colors (to get 256 colors in total). That means fixing the aforementioned two bugs results in a non-smooth color table because 254 does not divide into 256 * M - 1 cleanly.

So, this PR instead takes the approach of eschewing the Agg resampler entirely and performing the bilinear interpolation using straight NumPy. Now the extreme colors are exactly what they should be. I'll note that the tests still need to use a bit of atol because the definition of BiOrangeBlue uses input that is rounded to 3 decimal places. Update: now fixed.

>>> from matplotlib.cm import bivar_cmaps
>>> cmap = bivar_cmaps['BiOrangeBlue']
>>> print(cmap.patch[0, 0, :])
[0. 0. 0.]
>>> print(cmap.patch[-1, 0, :])
[1.  0.5 0. ]
>>> print(cmap.patch[-1, -1, :])
[1. 1. 1.]
>>> print(cmap.lut[0, 0, :])
[0. 0. 0. 1.]
>>> print(cmap.lut[-1, 0, :])
[1.  0.5 0.  1. ]
>>> print(cmap.lut[-1, -1, :])
[1. 1. 1. 1.]

PR checklist

@ayshih ayshih force-pushed the segmentedbivarcolormap_fix branch from 9f1bc1e to 9b4ec6d Compare December 8, 2025 20:03
@ayshih ayshih marked this pull request as ready for review December 8, 2025 20:24
@ayshih
Copy link
Contributor Author

ayshih commented Dec 8, 2025

As a sanity check of the 1D analogue, LinearSegmentedColormap, here's confirmation that "Greys" goes from pure white to pure black:

>>> import matplotlib as mpl
>>> greys = mpl.colormaps["Greys"]
>>> print(greys(0))
(np.float64(1.0), np.float64(1.0), np.float64(1.0), np.float64(1.0))
>>> print(greys(255))
(np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(1.0))

@ayshih ayshih force-pushed the segmentedbivarcolormap_fix branch from b3d74fc to 4c66b4e Compare December 9, 2025 14:45
Copy link
Contributor

@greglucas greglucas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this looks good to me and reads nice to do the math in numpy-space rather than Agg-space.

We should probably add a short change-note entry indicating this is fixing a minor bug in the interpolation scheme as it does change some results.

@greglucas
Copy link
Contributor

cc @trygvrad for a review

@greglucas greglucas requested a review from trygvrad December 9, 2025 15:31
@ayshih
Copy link
Contributor Author

ayshih commented Dec 9, 2025

I realized that because the BiOrangeBlue definition using a 9x9 color array is exactly linear in all directions – once the rounding to three decimal places was removed – its definition can be drastically simplified to just a 2x2 color array.

We should probably add a short change-note entry indicating this is fixing a minor bug in the interpolation scheme as it does change some results.

I've added an entry, but let me know if it's in the wrong place

@greglucas
Copy link
Contributor

I think it should be in next_api_changes similar to this changelog entry describing an update: https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.10.0.html#svg-output-improved-reproducibility

@trygvrad
Copy link
Contributor

trygvrad commented Dec 9, 2025

The code looks reasonable to me, it is good to get this fixed.

@ayshih If you are actively using bivariate colormaps, I would be interested it knowing more about your use case. We will need to add more bivariate colormaps to matplotlib as the project develops and in that context it is useful to hear from users.

@ayshih ayshih force-pushed the segmentedbivarcolormap_fix branch from 6baabe1 to 5a56a90 Compare December 11, 2025 13:57
@ayshih
Copy link
Contributor Author

ayshih commented Dec 11, 2025

I think it should be in next_api_changes

Ah, now I understand the system. Moved.

@ayshih If you are actively using bivariate colormaps, I would be interested it knowing more about your use case.

Heh, the bivariate colormaps look neat, but I haven't actually used them for anything. This PR came to be because I was befuddled why my fixes to the Agg resampler in #30184 affected colormaps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants