diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 185ec79ca8d8..7cf22b95623f 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -12,6 +12,7 @@ import matplotlib as mpl from . import _api, cbook +from .cm import ScalarMappable from .path import Path from .transforms import (Bbox, IdentityTransform, Transform, TransformedBbox, TransformedPatchPath, TransformedPath) @@ -1261,17 +1262,18 @@ def format_cursor_data(self, data): -------- get_cursor_data """ - if np.ndim(data) == 0 and getattr(self, "colorbar", None): + if np.ndim(data) == 0 and isinstance(self, ScalarMappable): # This block logically belongs to ScalarMappable, but can't be # implemented in it because most ScalarMappable subclasses inherit # from Artist first and from ScalarMappable second, so # Artist.format_cursor_data would always have precedence over # ScalarMappable.format_cursor_data. - return ( - "[" - + cbook.strip_math( - self.colorbar.formatter.format_data_short(data)).strip() - + "]") + n = self.cmap.N + # Midpoints of neighboring color intervals. + neighbors = self.norm.inverse( + (int(self.norm(data) * n) + np.array([0, 1])) / n) + delta = abs(neighbors - data).max() + return "[{:-#.{}g}]".format(data, cbook._g_sig_digits(data, delta)) else: try: data[0] diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 6d181c43107d..9d0fcadc431c 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -12,6 +12,7 @@ import functools import gzip import itertools +import math import operator import os from pathlib import Path @@ -2206,6 +2207,23 @@ def _format_approx(number, precision): return f'{number:.{precision}f}'.rstrip('0').rstrip('.') or '0' +def _g_sig_digits(value, delta): + """ + Return the number of significant digits to %g-format *value*, assuming that + it is known with an error of *delta*. + """ + # If e.g. value = 45.67 and delta = 0.02, then we want to round to 2 digits + # after the decimal point (floor(log10(0.02)) = -2); 45.67 contributes 2 + # digits before the decimal point (floor(log10(45.67)) + 1 = 2): the total + # is 4 significant digits. A value of 0 contributes 1 "digit" before the + # decimal point. + # For inf or nan, the precision doesn't matter. + return max( + 0, + (math.floor(math.log10(abs(value))) + 1 if value else 1) + - math.floor(math.log10(delta))) if math.isfinite(value) else 0 + + def _unikey_or_keysym_to_mplkey(unikey, keysym): """ Convert a Unicode key or X keysym to a Matplotlib key name. diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 322b84c6468d..7ce6292fb897 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -1399,16 +1399,10 @@ def format_coord(self, theta, r): # (as for linear axes), but for theta, use f-formatting as scientific # notation doesn't make sense and the trailing dot is ugly. def format_sig(value, delta, opt, fmt): - digits_post_decimal = math.floor(math.log10(delta)) - digits_offset = ( - # For "f", only count digits after decimal point. - 0 if fmt == "f" - # For "g", offset by digits before the decimal point. - else math.floor(math.log10(abs(value))) + 1 if value - # For "g", 0 contributes 1 "digit" before the decimal point. - else 1) - fmt_prec = max(0, digits_offset - digits_post_decimal) - return f"{value:-{opt}.{fmt_prec}{fmt}}" + # For "f", only count digits after decimal point. + prec = (max(0, -math.floor(math.log10(delta))) if fmt == "f" else + cbook._g_sig_digits(value, delta)) + return f"{value:-{opt}.{prec}{fmt}}" return ('\N{GREEK SMALL LETTER THETA}={}\N{GREEK SMALL LETTER PI} ' '({}\N{DEGREE SIGN}), r={}').format( diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 2e7fae6c58d8..9aab4e60a7a2 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -337,11 +337,11 @@ def test_cursor_data(): @pytest.mark.parametrize( - "data, text_without_colorbar, text_with_colorbar", [ - ([[10001, 10000]], "[1e+04]", "[10001]"), - ([[.123, .987]], "[0.123]", "[0.123]"), + "data, text", [ + ([[10001, 10000]], "[10001.000]"), + ([[.123, .987]], "[0.123]"), ]) -def test_format_cursor_data(data, text_without_colorbar, text_with_colorbar): +def test_format_cursor_data(data, text): from matplotlib.backend_bases import MouseEvent fig, ax = plt.subplots() @@ -350,15 +350,7 @@ def test_format_cursor_data(data, text_without_colorbar, text_with_colorbar): xdisp, ydisp = ax.transData.transform([0, 0]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == data[0][0] - assert im.format_cursor_data(im.get_cursor_data(event)) \ - == text_without_colorbar - - fig.colorbar(im) - fig.canvas.draw() # This is necessary to set up the colorbar formatter. - - assert im.get_cursor_data(event) == data[0][0] - assert im.format_cursor_data(im.get_cursor_data(event)) \ - == text_with_colorbar + assert im.format_cursor_data(im.get_cursor_data(event)) == text @image_comparison(['image_clip'], style='mpl20') diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 7a0380453452..6d8fa5419bbf 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -655,16 +655,7 @@ def format_data_short(self, value): # Rough approximation: no more than 1e4 divisions. a, b = self.axis.get_view_interval() delta = (b - a) / 1e4 - # If e.g. value = 45.67 and delta = 0.02, then we want to round to - # 2 digits after the decimal point (floor(log10(0.02)) = -2); - # 45.67 contributes 2 digits before the decimal point - # (floor(log10(45.67)) + 1 = 2): the total is 4 significant digits. - # A value of 0 contributes 1 "digit" before the decimal point. - sig_digits = max( - 0, - (math.floor(math.log10(abs(value))) + 1 if value else 1) - - math.floor(math.log10(delta))) - fmt = f"%-#.{sig_digits}g" + fmt = "%-#.{}g".format(cbook._g_sig_digits(value, delta)) return self._format_maybe_minus_and_locale(fmt, value) def format_data(self, value):