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

File tree

3 files changed

+100
-22
lines changed

3 files changed

+100
-22
lines changed

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy