diff --git a/doc/users/next_whats_new/2020-03-15-cursor-sigdigits.rst b/doc/users/next_whats_new/2020-03-15-cursor-sigdigits.rst new file mode 100644 index 000000000000..d340a3111d5a --- /dev/null +++ b/doc/users/next_whats_new/2020-03-15-cursor-sigdigits.rst @@ -0,0 +1,6 @@ +Cursor text now uses a number of significant digits matching pointing precision +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, the x/y position displayed by the cursor text would usually include +far more significant digits than the mouse pointing precision (typically one +pixel). This is now fixed for linear scales. diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 1495a1877e65..ae60b28267ac 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -558,6 +558,16 @@ def test_scilimits(self, sci_type, scilimits, lim, orderOfMag, fewticks): tmp_form.set_locs(ax.yaxis.get_majorticklocs()) assert orderOfMag == tmp_form.orderOfMagnitude + def test_cursor_precision(self): + fig, ax = plt.subplots() + ax.set_xlim(-1, 1) # Pointing precision of 0.001. + fmt = ax.xaxis.get_major_formatter().format_data_short + assert fmt(0.) == "0.000" + assert fmt(0.0123) == "0.012" + assert fmt(0.123) == "0.123" + assert fmt(1.23) == "1.230" + assert fmt(12.3) == "12.300" + class FakeAxis: """Allow Formatter to be called without having a "full" plot set up.""" diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index ff7bf99010d0..857c25c2fb8e 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -168,6 +168,7 @@ import logging import locale import math +from numbers import Integral import numpy as np @@ -587,11 +588,42 @@ def set_powerlimits(self, lims): def format_data_short(self, value): # docstring inherited + if isinstance(value, np.ma.MaskedArray) and value.mask: + return "" + if isinstance(value, Integral): + fmt = "%d" + else: + if self.axis.__name__ in ["xaxis", "yaxis"]: + if self.axis.__name__ == "xaxis": + axis_trf = self.axis.axes.get_xaxis_transform() + axis_inv_trf = axis_trf.inverted() + screen_xy = axis_trf.transform((value, 0)) + neighbor_values = axis_inv_trf.transform( + screen_xy + [[-1, 0], [+1, 0]])[:, 0] + else: # yaxis: + axis_trf = self.axis.axes.get_yaxis_transform() + axis_inv_trf = axis_trf.inverted() + screen_xy = axis_trf.transform((0, value)) + neighbor_values = axis_inv_trf.transform( + screen_xy + [[0, -1], [0, +1]])[:, 1] + delta = abs(neighbor_values - value).max() + else: + # Rough approximation: no more than 1e4 pixels. + delta = self.axis.get_view_interval() / 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" return ( - "" if isinstance(value, np.ma.MaskedArray) and value.mask else self.fix_minus( - locale.format_string("%-12g", (value,)) if self._useLocale else - "%-12g" % value)) + locale.format_string(fmt, (value,)) if self._useLocale else + fmt % value)) def format_data(self, value): # docstring inherited