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

Skip to content

Commit b00423f

Browse files
committed
Improve formatting of imshow() cursor data independently of colorbar.
Currently, when a colorbar is present, the cursor data under imshow() is formatted using the colorbar's cursor formatter (the idea being that that formatter should be able to "smartly" take normalization limits into account); if no colorbar is present a fixed format string ("%0.3g") is used. In fact, there is a better scale that defines the number of significant digits one should display in an imshow cursor data: it arises because colormaps are discrete (usually with 256 colors, but in any case the value is available as `cmap.N`). This quantization tells us, for a given value, by how much one needs to move before the underlying color changes (at all); that step size can be used to to determine a number of significant digits to display. (Even if that's not necessarily always the best number, it should at least be reasonable *given the user's choice of normalization*.) Also, note that because ScalarFormatter has now changed to take pixel size into account when determining *its* number of significant digits, the previous approach of relying on the colorbar formatter has become a less good approximation, as that means that the number of digits displayed for an imshow() cursor could depend on the physical size of the associated colorbar (if present). Also factor out and reuse some logic to compute the number of significant digits to use in format strings for given value/error pairs, already used by the linear and polar tickers.
1 parent 971d110 commit b00423f

File tree

5 files changed

+36
-39
lines changed

5 files changed

+36
-39
lines changed

lib/matplotlib/_api/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import functools
1414
import itertools
15+
import math
1516
import re
1617
import sys
1718
import warnings
@@ -297,3 +298,20 @@ def warn_external(message, category=None):
297298
break
298299
frame = frame.f_back
299300
warnings.warn(message, category, stacklevel)
301+
302+
303+
def g_sig_digits(value, delta):
304+
"""
305+
Return the number of significant digits to %g-format *value*, assuming that
306+
it is known with an error of *delta*.
307+
"""
308+
# If e.g. value = 45.67 and delta = 0.02, then we want to round to 2 digits
309+
# after the decimal point (floor(log10(0.02)) = -2); 45.67 contributes 2
310+
# digits before the decimal point (floor(log10(45.67)) + 1 = 2): the total
311+
# is 4 significant digits. A value of 0 contributes 1 "digit" before the
312+
# decimal point.
313+
# For inf or nan, the precision doesn't matter.
314+
return max(
315+
0,
316+
(math.floor(math.log10(abs(value))) + 1 if value else 1)
317+
- math.floor(math.log10(delta))) if math.isfinite(value) else 0

lib/matplotlib/artist.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import matplotlib as mpl
1414
from . import _api, cbook
15+
from .cm import ScalarMappable
1516
from .path import Path
1617
from .transforms import (Bbox, IdentityTransform, Transform, TransformedBbox,
1718
TransformedPatchPath, TransformedPath)
@@ -1258,17 +1259,18 @@ def format_cursor_data(self, data):
12581259
--------
12591260
get_cursor_data
12601261
"""
1261-
if np.ndim(data) == 0 and getattr(self, "colorbar", None):
1262+
if np.ndim(data) == 0 and isinstance(self, ScalarMappable):
12621263
# This block logically belongs to ScalarMappable, but can't be
12631264
# implemented in it because most ScalarMappable subclasses inherit
12641265
# from Artist first and from ScalarMappable second, so
12651266
# Artist.format_cursor_data would always have precedence over
12661267
# ScalarMappable.format_cursor_data.
1267-
return (
1268-
"["
1269-
+ cbook.strip_math(
1270-
self.colorbar.formatter.format_data_short(data)).strip()
1271-
+ "]")
1268+
n = self.cmap.N
1269+
# Midpoints of neighboring color intervals.
1270+
neighbors = self.norm.inverse(
1271+
(int(self.norm(data) * n) + np.array([0, 1])) / n)
1272+
delta = abs(neighbors - data).max()
1273+
return "[{:-#.{}g}]".format(data, _api.g_sig_digits(data, delta))
12721274
else:
12731275
try:
12741276
data[0]

lib/matplotlib/projections/polar.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,16 +1399,10 @@ def format_coord(self, theta, r):
13991399
# (as for linear axes), but for theta, use f-formatting as scientific
14001400
# notation doesn't make sense and the trailing dot is ugly.
14011401
def format_sig(value, delta, opt, fmt):
1402-
digits_post_decimal = math.floor(math.log10(delta))
1403-
digits_offset = (
1404-
# For "f", only count digits after decimal point.
1405-
0 if fmt == "f"
1406-
# For "g", offset by digits before the decimal point.
1407-
else math.floor(math.log10(abs(value))) + 1 if value
1408-
# For "g", 0 contributes 1 "digit" before the decimal point.
1409-
else 1)
1410-
fmt_prec = max(0, digits_offset - digits_post_decimal)
1411-
return f"{value:-{opt}.{fmt_prec}{fmt}}"
1402+
# For "f", only count digits after decimal point.
1403+
prec = (max(0, -math.floor(math.log10(delta))) if fmt == "f" else
1404+
_api.g_sig_digits(value, delta))
1405+
return f"{value:-{opt}.{prec}{fmt}}"
14121406

14131407
return ('\N{GREEK SMALL LETTER THETA}={}\N{GREEK SMALL LETTER PI} '
14141408
'({}\N{DEGREE SIGN}), r={}').format(

lib/matplotlib/tests/test_image.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,11 @@ def test_cursor_data():
337337

338338

339339
@pytest.mark.parametrize(
340-
"data, text_without_colorbar, text_with_colorbar", [
341-
([[10001, 10000]], "[1e+04]", "[10001]"),
342-
([[.123, .987]], "[0.123]", "[0.123]"),
340+
"data, text", [
341+
([[10001, 10000]], "[10001.000]"),
342+
([[.123, .987]], "[0.123]"),
343343
])
344-
def test_format_cursor_data(data, text_without_colorbar, text_with_colorbar):
344+
def test_format_cursor_data(data, text):
345345
from matplotlib.backend_bases import MouseEvent
346346

347347
fig, ax = plt.subplots()
@@ -350,15 +350,7 @@ def test_format_cursor_data(data, text_without_colorbar, text_with_colorbar):
350350
xdisp, ydisp = ax.transData.transform([0, 0])
351351
event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp)
352352
assert im.get_cursor_data(event) == data[0][0]
353-
assert im.format_cursor_data(im.get_cursor_data(event)) \
354-
== text_without_colorbar
355-
356-
fig.colorbar(im)
357-
fig.canvas.draw() # This is necessary to set up the colorbar formatter.
358-
359-
assert im.get_cursor_data(event) == data[0][0]
360-
assert im.format_cursor_data(im.get_cursor_data(event)) \
361-
== text_with_colorbar
353+
assert im.format_cursor_data(im.get_cursor_data(event)) == text
362354

363355

364356
@image_comparison(['image_clip'], style='mpl20')

lib/matplotlib/ticker.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -655,16 +655,7 @@ def format_data_short(self, value):
655655
# Rough approximation: no more than 1e4 divisions.
656656
a, b = self.axis.get_view_interval()
657657
delta = (b - a) / 1e4
658-
# If e.g. value = 45.67 and delta = 0.02, then we want to round to
659-
# 2 digits after the decimal point (floor(log10(0.02)) = -2);
660-
# 45.67 contributes 2 digits before the decimal point
661-
# (floor(log10(45.67)) + 1 = 2): the total is 4 significant digits.
662-
# A value of 0 contributes 1 "digit" before the decimal point.
663-
sig_digits = max(
664-
0,
665-
(math.floor(math.log10(abs(value))) + 1 if value else 1)
666-
- math.floor(math.log10(delta)))
667-
fmt = f"%-#.{sig_digits}g"
658+
fmt = "%-#.{}g".format(_api.g_sig_digits(value, delta))
668659
return self._format_maybe_minus_and_locale(fmt, value)
669660

670661
def format_data(self, value):

0 commit comments

Comments
 (0)