html_url,issue_url,id,node_id,user,created_at,updated_at,author_association,body,reactions,performed_via_github_app,issue https://github.com/pydata/xarray/issues/7014#issuecomment-1412027546,https://api.github.com/repos/pydata/xarray/issues/7014,1412027546,IC_kwDOAMm_X85UKdSa,13662783,2023-02-01T13:06:09Z,2023-02-01T13:06:45Z,CONTRIBUTOR,"Debugging this, @headtr1ck points correctly to _determine_cmap_params: ```python if levels is not None or isinstance(norm, mpl.colors.BoundaryNorm): cmap, newnorm = _build_discrete_cmap(cmap, levels, extend, filled) norm = newnorm if norm is None else norm ``` The problem lies in the second line. In `_build_discrete_cmap`, a new cmap is returned with a different number of levels. However, if the original `norm` is not None, you end up with a mismatch, as the old norm expects the old cmap. What then happens, is that the norm calls into the cmap. Calling into cmap doesn't do any checks whether the value is larger than N, it just takes the highest available value. The examples in #4061 show this quite clearly, but to illustrate: ```python import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np data = np.arange(100).reshape((10, 10)) cmap = mpl.cm.get_cmap(""viridis"") print(cmap.N) # 256 boundaries = [0, 25, 50, 75, 100] norm = mpl.colors.BoundaryNorm(boundaries, cmap.N) fig, ax = plt.subplots() ax.imshow(data, norm=norm, cmap=cmap) # %% colors = [cmap(i/255) for i in np.linspace(0, cmap.N, len(boundaries) - 1)] new_cmap, new_norm = mpl.colors.from_levels_and_colors(boundaries, colors) print(new_cmap.N) # 4 fig, ax = plt.subplots() ax.imshow(data, norm=new_norm, cmap=new_cmap) # %% # Mismatched fig, ax = plt.subplots() ax.imshow(data, norm=norm, cmap=new_cmap) # %% ``` This is avoided here by removing the conditional in the second line, or just making sure both cmap and norm are replaced by their new values: ```python if levels is not None or isinstance(norm, mpl.colors.BoundaryNorm): cmap, norm = _build_discrete_cmap(cmap, levels, extend, filled) ``` Then, the cmap and norm remain in sync. However, when running the tests in `test_plot.py`, this gives a single questionable(?) failure: ```python def test_norm_sets_vmin_vmax(self) -> None: vmin = self.data.min() vmax = self.data.max() for norm, extend, levels in zip( [ mpl.colors.Normalize(), mpl.colors.Normalize(), mpl.colors.Normalize(vmin + 0.1, vmax - 0.1), mpl.colors.Normalize(None, vmax - 0.1), mpl.colors.Normalize(vmin + 0.1, None), ], [""neither"", ""neither"", ""both"", ""max"", ""min""], [7, None, None, None, None], ): test_min = vmin if norm.vmin is None else norm.vmin test_max = vmax if norm.vmax is None else norm.vmax cmap_params = _determine_cmap_params(self.data, norm=norm, levels=levels) assert cmap_params[""vmin""] is None assert cmap_params[""vmax""] is None assert cmap_params[""norm""].vmin == test_min assert cmap_params[""norm""].vmax == test_max assert cmap_params[""extend""] == extend > assert cmap_params[""norm""] == norm E assert == ``` I don't understand why the conditional is there. At first sight, it doesn't make a lot of sense to create a new norm and cmap, but then take the original norm? ","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",,1368027148