Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3e4b469

Browse files
committed
Fix for mouseover image data with a MultiNorm
If an image is shown, and the user hovers over the image, the value at that coordinate should be displayed. The number formating is handled by colorizer.Colorizer._format_cursor_data_override(). This is now updated to also handle the case where a MultiNorm is used (i.e. a bivariat or multivariate colormap), in which case multiple values are displayed.
1 parent 9c26a1e commit 3e4b469

3 files changed

Lines changed: 100 additions & 22 deletions

File tree

lib/matplotlib/colorizer.py

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -451,39 +451,63 @@ def colorbar(self):
451451
def colorbar(self, colorbar):
452452
self._colorizer.colorbar = colorbar
453453

454-
def _format_cursor_data_override(self, data):
455-
# This function overwrites Artist.format_cursor_data(). We cannot
456-
# implement cm.ScalarMappable.format_cursor_data() directly, because
457-
# most cm.ScalarMappable subclasses inherit from Artist first and from
458-
# cm.ScalarMappable second, so Artist.format_cursor_data would always
459-
# have precedence over cm.ScalarMappable.format_cursor_data.
460-
461-
# Note if cm.ScalarMappable is depreciated, this functionality should be
462-
# implemented as format_cursor_data() on ColorizingArtist.
463-
n = self.cmap.N
464-
if np.ma.getmask(data):
465-
return "[]"
466-
normed = self.norm(data)
454+
@staticmethod
455+
def _sig_digits_from_norm(norm, data, n):
456+
# Determines the number of significant digits
457+
# to use for a number given a norm, and n, where n is the
458+
# number of colors in the colormap.
459+
normed = norm(data)
467460
if np.isfinite(normed):
468-
if isinstance(self.norm, colors.BoundaryNorm):
461+
if isinstance(norm, colors.BoundaryNorm):
469462
# not an invertible normalization mapping
470-
cur_idx = np.argmin(np.abs(self.norm.boundaries - data))
463+
cur_idx = np.argmin(np.abs(norm.boundaries - data))
471464
neigh_idx = max(0, cur_idx - 1)
472465
# use max diff to prevent delta == 0
473466
delta = np.diff(
474-
self.norm.boundaries[neigh_idx:cur_idx + 2]
467+
norm.boundaries[neigh_idx:cur_idx + 2]
475468
).max()
476-
elif self.norm.vmin == self.norm.vmax:
469+
elif norm.vmin == norm.vmax:
477470
# singular norms, use delta of 10% of only value
478-
delta = np.abs(self.norm.vmin * .1)
471+
delta = np.abs(norm.vmin * .1)
479472
else:
480473
# Midpoints of neighboring color intervals.
481-
neighbors = self.norm.inverse(
474+
neighbors = norm.inverse(
482475
(int(normed * n) + np.array([0, 1])) / n)
483476
delta = abs(neighbors - data).max()
484477
g_sig_digits = cbook._g_sig_digits(data, delta)
485478
else:
486479
g_sig_digits = 3 # Consistent with default below.
480+
return g_sig_digits
481+
482+
def _format_cursor_data_override(self, data):
483+
# This function overwrites Artist.format_cursor_data(). We cannot
484+
# implement cm.ScalarMappable.format_cursor_data() directly, because
485+
# most cm.ScalarMappable subclasses inherit from Artist first and from
486+
# cm.ScalarMappable second, so Artist.format_cursor_data would always
487+
# have precedence over cm.ScalarMappable.format_cursor_data.
488+
489+
# Note if cm.ScalarMappable is depreciated, this functionality should be
490+
# implemented as format_cursor_data() on ColorizingArtist.
491+
if np.ma.getmask(data) or data is None:
492+
return "[]"
493+
if len(data.dtype.descr) > 1:
494+
# We have multivariate data encoded as a data type with multiple fields
495+
# NOTE: If any of the fields are masked, "[]" would be returned via
496+
# the if statement above.
497+
s_sig_digits_list = []
498+
if isinstance(self.cmap, colors.BivarColormap):
499+
n_s = (self.cmap.N, self.cmap.M)
500+
else:
501+
n_s = [part.N for part in self.cmap]
502+
os = [f"{d:-#.{self._sig_digits_from_norm(no, d, n)}g}"
503+
for no, d, n in zip(self.norm.norms, data, n_s)]
504+
return f"[{', '.join(os)}]"
505+
506+
# scalar data
507+
n = self.cmap.N
508+
g_sig_digits = self._sig_digits_from_norm(self.norm,
509+
data,
510+
n)
487511
return f"[{data:-#.{g_sig_digits}g}]"
488512

489513

lib/matplotlib/tests/test_image.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import matplotlib as mpl
1616
from matplotlib import (
17-
colors, image as mimage, patches, pyplot as plt, style, rcParams)
17+
cbook, colors, image as mimage, patches, pyplot as plt, style, rcParams)
1818
from matplotlib.image import (AxesImage, BboxImage, FigureImage,
1919
NonUniformImage, PcolorImage)
2020
from matplotlib.testing.decorators import check_figures_equal, image_comparison
@@ -1130,8 +1130,14 @@ def test_image_cursor_formatting():
11301130
data = np.ma.masked_array([0], mask=[False])
11311131
assert im.format_cursor_data(data) == '[0]'
11321132

1133-
data = np.nan
1134-
assert im.format_cursor_data(data) == '[nan]'
1133+
# This used to test
1134+
# > data = np.nan
1135+
# > assert im.format_cursor_data(data) == '[nan]'
1136+
# However, a value of nan will be masked by `cbook.safe_masked_invalid(data)`
1137+
# called by `image._ImageBase._normalize_image_array(data)`
1138+
# The test is therefore changed to:
1139+
data = cbook.safe_masked_invalid(np.nan)
1140+
assert im.format_cursor_data(data) == '[]'
11351141

11361142

11371143
@check_figures_equal()

lib/matplotlib/tests/test_multivariate_axes.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,54 @@ def test_cmap_error():
629629
mpl.collections.PatchCollection([], cmap='not_a_cmap')
630630

631631

632+
def test_artist_format_cursor_data_multivar():
633+
634+
X = np.zeros((4, 3))
635+
X[0, 0] = 0.9
636+
X[0, 1] = 0.99
637+
X[0, 2] = 0.999
638+
X[1, 0] = -1
639+
X[1, 1] = 0
640+
X[1, 2] = 1
641+
X[2, 0] = 0.09
642+
X[2, 1] = 0.009
643+
X[2, 2] = 0.0009
644+
X[3, 0] = np.nan
645+
646+
Y = np.arange(np.prod(X.shape)).reshape(X.shape)
647+
648+
labels_list = [
649+
"[0.9, 0.00]",
650+
"[1., 1.00]",
651+
"[1., 2.00]",
652+
"[-1.0, 3.00]",
653+
"[0.0, 4.00]",
654+
"[1.0, 5.00]",
655+
"[0.09, 6.00]",
656+
"[0.009, 7.00]",
657+
"[0.0009, 8.00]",
658+
"[]",
659+
]
660+
661+
pos = [[0, 0], [1, 0], [2, 0],
662+
[0, 1], [1, 1], [2, 1],
663+
[0, 2], [1, 2], [2, 2],
664+
[3, 0]]
665+
666+
from matplotlib.backend_bases import MouseEvent
667+
668+
for cmap in ['BiOrangeBlue', '2VarAddA']:
669+
fig, ax = plt.subplots()
670+
norm = mpl.colors.BoundaryNorm(np.linspace(-1, 1, 20), 256)
671+
data = (X, Y)
672+
im = ax.imshow(data, cmap=cmap, norm=(norm, None))
673+
674+
for v, text in zip(pos, labels_list):
675+
xdisp, ydisp = ax.transData.transform(v)
676+
event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp)
677+
assert im.format_cursor_data(im.get_cursor_data(event)) == text
678+
679+
632680
def test_multivariate_safe_masked_invalid():
633681
dt = np.dtype('float32, float32').newbyteorder('>')
634682
x = np.zeros(2, dtype=dt)

0 commit comments

Comments
 (0)