From ffad256999f5683fd6080b627875a3320a3cb1d6 Mon Sep 17 00:00:00 2001 From: Kayran Schmidt Date: Wed, 13 Apr 2022 00:18:53 +0200 Subject: [PATCH 1/5] Fix #21915 --- lib/matplotlib/artist.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 1f33b9d3ec11..0f47035ed99c 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -12,6 +12,7 @@ import matplotlib as mpl from . import _api, cbook +from .colors import BoundaryNorm from .cm import ScalarMappable from .path import Path from .transforms import (Bbox, IdentityTransform, Transform, TransformedBbox, @@ -1303,10 +1304,22 @@ def format_cursor_data(self, data): return "[]" normed = self.norm(data) if np.isfinite(normed): - # Midpoints of neighboring color intervals. - neighbors = self.norm.inverse( - (int(self.norm(data) * n) + np.array([0, 1])) / n) - delta = abs(neighbors - data).max() + if isinstance(self.norm, BoundaryNorm): + # not an invertible normalization mapping + cur_idx = np.argmin(np.abs(self.norm.boundaries - data)) + neigh_idx1 = max(0, cur_idx - 1) + neigh_idx2 = min( + len(self.norm.boundaries) - 1, cur_idx + 1) + # use max diff to prevent delta == 0 + delta = np.diff( + self.norm.boundaries[[neigh_idx1, cur_idx, neigh_idx2]] + ).max() + + else: + # Midpoints of neighboring color intervals. + neighbors = self.norm.inverse( + (int(normed * n) + np.array([0, 1])) / n) + delta = abs(neighbors - data).max() g_sig_digits = cbook._g_sig_digits(data, delta) else: g_sig_digits = 3 # Consistent with default below. From 50d413d79f3316dc1cdc98145f5191b32df74a45 Mon Sep 17 00:00:00 2001 From: Kayran Schmidt Date: Wed, 13 Apr 2022 00:59:02 +0200 Subject: [PATCH 2/5] Add test for BoundaryNorm cursor data output --- lib/matplotlib/tests/test_artist.py | 116 ++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index a9324f0bea58..adccd06d8d76 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -5,6 +5,8 @@ import pytest +from matplotlib import cbook, cm +import matplotlib.colors as mcolors import matplotlib.pyplot as plt import matplotlib.patches as mpatches import matplotlib.lines as mlines @@ -372,3 +374,117 @@ class MyArtist4(MyArtist3): pass assert MyArtist4.set is MyArtist3.set + + +def test_format_cursor_data_BoundaryNorm(): + """Test if cursor data is correct when using BoundaryNorm.""" + X = np.empty((3, 3)) + X[0, 0] = 0.9 + X[0, 1] = 0.99 + X[0, 2] = 0.999 + X[1, 0] = -1 + X[1, 1] = 0 + X[1, 2] = 1 + X[2, 0] = 0.09 + X[2, 1] = 0.009 + X[2, 2] = 0.0009 + + # map range -1..1 to 0..256 in 0.1 steps + fig, ax = plt.subplots() + fig.suptitle("-1..1 to 0..256 in 0.1") + norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 20), 256) + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v in X.flat: + label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.1)) + assert img.format_cursor_data(v) == label + + plt.close() + + # map range -1..1 to 0..256 in 0.01 steps + fig, ax = plt.subplots() + fig.suptitle("-1..1 to 0..256 in 0.01") + cmap = cm.get_cmap('RdBu_r', 200) + norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 200), 200) + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v in X.flat: + label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.01)) + assert img.format_cursor_data(v) == label + + plt.close() + + # map range -1..1 to 0..256 in 0.01 steps + fig, ax = plt.subplots() + fig.suptitle("-1..1 to 0..256 in 0.001") + cmap = cm.get_cmap('RdBu_r', 2000) + norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 2000), 2000) + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v in X.flat: + label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.001)) + assert img.format_cursor_data(v) == label + + plt.close() + + # out of bounds values for 0..1 + Y = np.empty((7, 1)) + Y[0] = -1.0 + Y[1] = 0.0 + Y[2] = 0.1 + Y[3] = 0.5 + Y[4] = 0.9 + Y[5] = 1.0 + Y[6] = 2.0 + + fig, ax = plt.subplots() + fig.suptitle("noclip, neither") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='neither') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v in X.flat: + label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() + + fig, ax = plt.subplots() + fig.suptitle("noclip, min") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='min') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v in X.flat: + label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() + + fig, ax = plt.subplots() + fig.suptitle("noclip, max") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='max') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v in X.flat: + label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() + + fig, ax = plt.subplots() + fig.suptitle("noclip, both") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='both') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v in X.flat: + label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() + + fig, ax = plt.subplots() + fig.suptitle("clip, neither") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=True, extend='neither') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v in X.flat: + label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() From 455abb4646867d52fdd8cdff6e0fc91105152935 Mon Sep 17 00:00:00 2001 From: Kayran Schmidt Date: Wed, 13 Apr 2022 18:56:00 +0200 Subject: [PATCH 3/5] Use slicing for cleaner code --- lib/matplotlib/artist.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 0f47035ed99c..24a60fd4d121 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -1307,12 +1307,10 @@ def format_cursor_data(self, data): if isinstance(self.norm, BoundaryNorm): # not an invertible normalization mapping cur_idx = np.argmin(np.abs(self.norm.boundaries - data)) - neigh_idx1 = max(0, cur_idx - 1) - neigh_idx2 = min( - len(self.norm.boundaries) - 1, cur_idx + 1) + neigh_idx = max(0, cur_idx - 1) # use max diff to prevent delta == 0 delta = np.diff( - self.norm.boundaries[[neigh_idx1, cur_idx, neigh_idx2]] + self.norm.boundaries[neigh_idx:cur_idx + 2] ).max() else: From e75e8ea7c81061b652fe7a40d77bca53cd95722a Mon Sep 17 00:00:00 2001 From: Kayran Schmidt Date: Thu, 21 Apr 2022 12:44:26 +0200 Subject: [PATCH 4/5] Use resampled colormaps --- lib/matplotlib/tests/test_artist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index adccd06d8d76..13da16b52c6f 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -405,7 +405,7 @@ def test_format_cursor_data_BoundaryNorm(): fig.suptitle("-1..1 to 0..256 in 0.01") cmap = cm.get_cmap('RdBu_r', 200) norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 200), 200) - img = ax.imshow(X, cmap='RdBu_r', norm=norm) + img = ax.imshow(X, cmap=cmap, norm=norm) for v in X.flat: label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.01)) assert img.format_cursor_data(v) == label @@ -417,7 +417,7 @@ def test_format_cursor_data_BoundaryNorm(): fig.suptitle("-1..1 to 0..256 in 0.001") cmap = cm.get_cmap('RdBu_r', 2000) norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 2000), 2000) - img = ax.imshow(X, cmap='RdBu_r', norm=norm) + img = ax.imshow(X, cmap=cmap, norm=norm) for v in X.flat: label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.001)) assert img.format_cursor_data(v) == label From ec8b93e21b3d9962543ae85c2776195eb594d27f Mon Sep 17 00:00:00 2001 From: Kayran Schmidt Date: Thu, 23 Jun 2022 13:51:35 +0200 Subject: [PATCH 5/5] Test against explicit, predefined labels --- lib/matplotlib/tests/test_artist.py | 99 +++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index 13da16b52c6f..0fb1fa442fe7 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -5,7 +5,7 @@ import pytest -from matplotlib import cbook, cm +from matplotlib import cm import matplotlib.colors as mcolors import matplotlib.pyplot as plt import matplotlib.patches as mpatches @@ -394,8 +394,20 @@ def test_format_cursor_data_BoundaryNorm(): fig.suptitle("-1..1 to 0..256 in 0.1") norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 20), 256) img = ax.imshow(X, cmap='RdBu_r', norm=norm) - for v in X.flat: - label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.1)) + + labels_list = [ + "[0.9]", + "[1.]", + "[1.]", + "[-1.0]", + "[0.0]", + "[1.0]", + "[0.09]", + "[0.009]", + "[0.0009]", + ] + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.1)) assert img.format_cursor_data(v) == label plt.close() @@ -406,8 +418,20 @@ def test_format_cursor_data_BoundaryNorm(): cmap = cm.get_cmap('RdBu_r', 200) norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 200), 200) img = ax.imshow(X, cmap=cmap, norm=norm) - for v in X.flat: - label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.01)) + + labels_list = [ + "[0.90]", + "[0.99]", + "[1.0]", + "[-1.00]", + "[0.00]", + "[1.00]", + "[0.09]", + "[0.009]", + "[0.0009]", + ] + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.01)) assert img.format_cursor_data(v) == label plt.close() @@ -418,29 +442,52 @@ def test_format_cursor_data_BoundaryNorm(): cmap = cm.get_cmap('RdBu_r', 2000) norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 2000), 2000) img = ax.imshow(X, cmap=cmap, norm=norm) - for v in X.flat: - label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.001)) + + labels_list = [ + "[0.900]", + "[0.990]", + "[0.999]", + "[-1.000]", + "[0.000]", + "[1.000]", + "[0.090]", + "[0.009]", + "[0.0009]", + ] + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.001)) assert img.format_cursor_data(v) == label plt.close() - # out of bounds values for 0..1 - Y = np.empty((7, 1)) - Y[0] = -1.0 - Y[1] = 0.0 - Y[2] = 0.1 - Y[3] = 0.5 - Y[4] = 0.9 - Y[5] = 1.0 - Y[6] = 2.0 + # different testing data set with + # out of bounds values for 0..1 range + X = np.empty((7, 1)) + X[0] = -1.0 + X[1] = 0.0 + X[2] = 0.1 + X[3] = 0.5 + X[4] = 0.9 + X[5] = 1.0 + X[6] = 2.0 + + labels_list = [ + "[-1.0]", + "[0.0]", + "[0.1]", + "[0.5]", + "[0.9]", + "[1.0]", + "[2.0]", + ] fig, ax = plt.subplots() fig.suptitle("noclip, neither") norm = mcolors.BoundaryNorm( np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='neither') img = ax.imshow(X, cmap='RdBu_r', norm=norm) - for v in X.flat: - label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) assert img.format_cursor_data(v) == label plt.close() @@ -450,8 +497,8 @@ def test_format_cursor_data_BoundaryNorm(): norm = mcolors.BoundaryNorm( np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='min') img = ax.imshow(X, cmap='RdBu_r', norm=norm) - for v in X.flat: - label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) assert img.format_cursor_data(v) == label plt.close() @@ -461,8 +508,8 @@ def test_format_cursor_data_BoundaryNorm(): norm = mcolors.BoundaryNorm( np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='max') img = ax.imshow(X, cmap='RdBu_r', norm=norm) - for v in X.flat: - label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) assert img.format_cursor_data(v) == label plt.close() @@ -472,8 +519,8 @@ def test_format_cursor_data_BoundaryNorm(): norm = mcolors.BoundaryNorm( np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='both') img = ax.imshow(X, cmap='RdBu_r', norm=norm) - for v in X.flat: - label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) assert img.format_cursor_data(v) == label plt.close() @@ -483,8 +530,8 @@ def test_format_cursor_data_BoundaryNorm(): norm = mcolors.BoundaryNorm( np.linspace(0, 1, 4, endpoint=True), 256, clip=True, extend='neither') img = ax.imshow(X, cmap='RdBu_r', norm=norm) - for v in X.flat: - label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) assert img.format_cursor_data(v) == label plt.close()