diff --git a/doc/api/next_api_changes/deprecations/22507-AL.rst b/doc/api/next_api_changes/deprecations/22507-AL.rst new file mode 100644 index 000000000000..c71c92e0ad93 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/22507-AL.rst @@ -0,0 +1,5 @@ +The *math* parameter of ``mathtext.get_unicode_index`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In math mode, ASCII hyphens (U+002D) are now replaced by unicode minus signs +(U+2212) at the parsing stage. diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 551898d546ac..eb849d8d4823 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -18,7 +18,7 @@ QuotedString, Regex, StringEnd, ZeroOrMore, pyparsing_common) import matplotlib as mpl -from . import cbook +from . import _api, cbook from ._mathtext_data import ( latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni) from .font_manager import FontProperties, findfont, get_font @@ -33,7 +33,8 @@ # FONTS -def get_unicode_index(symbol, math=True): +@_api.delete_parameter("3.6", "math") +def get_unicode_index(symbol, math=True): # Publicly exported. r""" Return the integer index (from the Unicode table) of *symbol*. @@ -45,15 +46,13 @@ def get_unicode_index(symbol, math=True): math : bool, default: True If False, always treat as a single Unicode character. """ - # for a non-math symbol, simply return its Unicode index - if not math: - return ord(symbol) # From UTF #25: U+2212 minus sign is the preferred # representation of the unary and binary minus sign rather than # the ASCII-derived U+002D hyphen-minus, because minus sign is # unambiguous and because it is rendered with a more desirable # length, usually longer than a hyphen. - if symbol == '-': + # Remove this block when the 'math' parameter is deleted. + if math and symbol == '-': return 0x2212 try: # This will succeed if symbol is a single Unicode char return ord(symbol) @@ -98,7 +97,7 @@ def get_kern(self, font1, fontclass1, sym1, fontsize1, """ return 0. - def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True): + def get_metrics(self, font, font_class, sym, fontsize, dpi): r""" Parameters ---------- @@ -117,8 +116,6 @@ def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True): Font size in points. dpi : float Rendering dots-per-inch. - math : bool - Whether we are currently in math mode or not. Returns ------- @@ -136,7 +133,7 @@ def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True): - *slanted*: Whether the glyph should be considered as "slanted" (currently used for kerning sub/superscripts). """ - info = self._get_info(font, font_class, sym, fontsize, dpi, math) + info = self._get_info(font, font_class, sym, fontsize, dpi) return info.metrics def render_glyph(self, ox, oy, font, font_class, sym, fontsize, dpi): @@ -217,14 +214,14 @@ def _get_offset(self, font, glyph, fontsize, dpi): return (glyph.height / 64 / 2) + (fontsize/3 * dpi/72) return 0. - def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): + def _get_info(self, fontname, font_class, sym, fontsize, dpi): key = fontname, font_class, sym, fontsize, dpi bunch = self.glyphd.get(key) if bunch is not None: return bunch font, num, slanted = self._get_glyph( - fontname, font_class, sym, fontsize, math) + fontname, font_class, sym, fontsize) font.set_size(fontsize, dpi) glyph = font.load_char( @@ -314,7 +311,7 @@ def __init__(self, *args, **kwargs): _slanted_symbols = set(r"\int \oint".split()) - def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): + def _get_glyph(self, fontname, font_class, sym, fontsize): font = None if fontname in self.fontmap and sym in latex_to_bakoma: basename, num = latex_to_bakoma[sym] @@ -329,7 +326,7 @@ def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): return font, num, slanted else: return self._stix_fallback._get_glyph( - fontname, font_class, sym, fontsize, math) + fontname, font_class, sym, fontsize) # The Bakoma fonts contain many pre-sized alternatives for the # delimiters. The AutoSizedChar class will use these alternatives @@ -442,9 +439,9 @@ def __init__(self, *args, **kwargs): def _map_virtual_font(self, fontname, font_class, uniindex): return fontname, uniindex - def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): + def _get_glyph(self, fontname, font_class, sym, fontsize): try: - uniindex = get_unicode_index(sym, math) + uniindex = get_unicode_index(sym) found_symbol = True except ValueError: uniindex = ord('?') @@ -536,11 +533,10 @@ def __init__(self, *args, **kwargs): self.fontmap[key] = fullpath self.fontmap[name] = fullpath - def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): + def _get_glyph(self, fontname, font_class, sym, fontsize): # Override prime symbol to use Bakoma. if sym == r'\prime': - return self.bakoma._get_glyph( - fontname, font_class, sym, fontsize, math) + return self.bakoma._get_glyph(fontname, font_class, sym, fontsize) else: # check whether the glyph is available in the display font uniindex = get_unicode_index(sym) @@ -548,11 +544,9 @@ def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): if font is not None: glyphindex = font.get_char_index(uniindex) if glyphindex != 0: - return super()._get_glyph( - 'ex', font_class, sym, fontsize, math) + return super()._get_glyph('ex', font_class, sym, fontsize) # otherwise return regular glyph - return super()._get_glyph( - fontname, font_class, sym, fontsize, math) + return super()._get_glyph(fontname, font_class, sym, fontsize) class DejaVuSerifFonts(DejaVuFonts): @@ -913,7 +907,7 @@ class Char(Node): `Hlist`. """ - def __init__(self, c, state, math=True): + def __init__(self, c, state): super().__init__() self.c = c self.font_output = state.font_output @@ -921,7 +915,6 @@ def __init__(self, c, state, math=True): self.font_class = state.font_class self.fontsize = state.fontsize self.dpi = state.dpi - self.math = math # The real width, height and depth will be set during the # pack phase, after we know the real fontsize self._update_metrics() @@ -931,8 +924,7 @@ def __repr__(self): def _update_metrics(self): metrics = self._metrics = self.font_output.get_metrics( - self.font, self.font_class, self.c, self.fontsize, self.dpi, - self.math) + self.font, self.font_class, self.c, self.fontsize, self.dpi) if self.c == ' ': self.width = metrics.advance else: @@ -1624,8 +1616,9 @@ class _MathStyle(enum.Enum): SCRIPTSTYLE = enum.auto() SCRIPTSCRIPTSTYLE = enum.auto() - _binary_operators = set(r''' - + * - + _binary_operators = set( + '+ * - \N{MINUS SIGN}' + r''' \pm \sqcap \rhd \mp \sqcup \unlhd \times \vee \unrhd @@ -1922,7 +1915,7 @@ def math(self, s, loc, toks): def non_math(self, s, loc, toks): s = toks[0].replace(r'\$', '$') - symbols = [Char(c, self.get_state(), math=False) for c in s] + symbols = [Char(c, self.get_state()) for c in s] hlist = Hlist(symbols) # We're going into math now, so set font to 'it' self.push_state() @@ -1969,6 +1962,13 @@ def customspace(self, s, loc, toks): def symbol(self, s, loc, toks): c = toks["sym"] + if c == "-": + # "U+2212 minus sign is the preferred representation of the unary + # and binary minus sign rather than the ASCII-derived U+002D + # hyphen-minus, because minus sign is unambiguous and because it + # is rendered with a more desirable length, usually longer than a + # hyphen." (https://www.unicode.org/reports/tr25/) + c = "\N{MINUS SIGN}" try: char = Char(c, self.get_state()) except ValueError as err: diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index a60634731b6b..8dac9301ed81 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -132,7 +132,7 @@ ']' : ('cmr10', 0x5d), '*' : ('cmsy10', 0xa4), - '-' : ('cmsy10', 0xa1), + '\N{MINUS SIGN}' : ('cmsy10', 0xa1), '\\Downarrow' : ('cmsy10', 0x2b), '\\Im' : ('cmsy10', 0x3d), '\\Leftarrow' : ('cmsy10', 0x28), diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf deleted file mode 100644 index c76b653c33fb..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png deleted file mode 100644 index e305de1d9ac7..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg deleted file mode 100644 index 0a29c9d0af21..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg +++ /dev/null @@ -1,670 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 72e5f63cd2ab..9118e8d205f8 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2628,16 +2628,48 @@ def test_pyplot_axes(): plt.close(fig2) -@image_comparison(['log_scales']) def test_log_scales(): - # Remove this if regenerating the image. - plt.rcParams['axes.unicode_minus'] = False - fig, ax = plt.subplots() ax.plot(np.log(np.linspace(0.1, 100))) ax.set_yscale('log', base=5.5) ax.invert_yaxis() ax.set_xscale('log', base=9.0) + xticks, yticks = [ + [(t.get_loc(), t.label1.get_text()) for t in axis._update_ticks()] + for axis in [ax.xaxis, ax.yaxis] + ] + assert xticks == [ + (1.0, '$\\mathdefault{9^{0}}$'), + (9.0, '$\\mathdefault{9^{1}}$'), + (81.0, '$\\mathdefault{9^{2}}$'), + (2.0, ''), + (3.0, ''), + (4.0, ''), + (5.0, ''), + (6.0, ''), + (7.0, ''), + (8.0, ''), + (18.0, ''), + (27.0, ''), + (36.0, ''), + (45.0, ''), + (54.0, ''), + (63.0, ''), + (72.0, ''), + ] + assert yticks == [ + (0.18181818181818182, '$\\mathdefault{5.5^{-1}}$'), + (1.0, '$\\mathdefault{5.5^{0}}$'), + (5.5, '$\\mathdefault{5.5^{1}}$'), + (0.36363636363636365, ''), + (0.5454545454545454, ''), + (0.7272727272727273, ''), + (0.9090909090909092, ''), + (2.0, ''), + (3.0, ''), + (4.0, ''), + (5.0, ''), + ] def test_log_scales_no_data(): diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index ae3ab92c0d43..33b92495b740 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -609,7 +609,7 @@ def test_colorbar_format(fmt): im.set_norm(LogNorm(vmin=0.1, vmax=10)) fig.canvas.draw() assert (cbar.ax.yaxis.get_ticklabels()[0].get_text() == - '$\\mathdefault{10^{\N{Minus Sign}2}}$') + '$\\mathdefault{10^{-2}}$') def test_colorbar_scale_reset(): diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index d8c8c7d9e764..a89a7634feda 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -769,7 +769,7 @@ class TestLogFormatterMathtext: @pytest.mark.parametrize('min_exponent, value, expected', test_data) def test_min_exponent(self, min_exponent, value, expected): with mpl.rc_context({'axes.formatter.min_exponent': min_exponent}): - assert self.fmt(value) == expected.replace('-', '\N{Minus Sign}') + assert self.fmt(value) == expected class TestLogFormatterSciNotation: @@ -798,7 +798,7 @@ def test_basic(self, base, value, expected): formatter = mticker.LogFormatterSciNotation(base=base) formatter.sublabel = {1, 2, 5, 1.2} with mpl.rc_context({'text.usetex': False}): - assert formatter(value) == expected.replace('-', '\N{Minus Sign}') + assert formatter(value) == expected class TestLogFormatter: @@ -1016,18 +1016,17 @@ def logit_deformatter(string): """ match = re.match( r"[^\d]*" - r"(?P1[-\N{Minus Sign}])?" + r"(?P1-)?" r"(?P\d*\.?\d*)?" r"(?:\\cdot)?" - r"(?:10\^\{(?P[-\N{Minus Sign}]?\d*)})?" + r"(?:10\^\{(?P-?\d*)})?" r"[^\d]*$", string, ) if match: comp = match["comp"] is not None mantissa = float(match["mant"]) if match["mant"] else 1 - expo = (int(match["expo"].replace("\N{Minus Sign}", "-")) - if match["expo"] is not None else 0) + expo = int(match["expo"]) if match["expo"] is not None else 0 value = mantissa * 10 ** expo if match["mant"] or match["expo"] is not None: if comp: @@ -1152,8 +1151,8 @@ def test_use_overline(self): Test the parameter use_overline """ x = 1 - 1e-2 - fx1 = "$\\mathdefault{1\N{Minus Sign}10^{\N{Minus Sign}2}}$" - fx2 = "$\\mathdefault{\\overline{10^{\N{Minus Sign}2}}}$" + fx1 = r"$\mathdefault{1-10^{-2}}$" + fx2 = r"$\mathdefault{\overline{10^{-2}}}$" form = mticker.LogitFormatter(use_overline=False) assert form(x) == fx1 form.use_overline(True) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 1610f55a74a9..38ed75f12fdd 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1100,12 +1100,11 @@ def __call__(self, x, pos=None): base = '%s' % b if abs(fx) < min_exp: - s = r'$\mathdefault{%s%g}$' % (sign_string, x) + return r'$\mathdefault{%s%g}$' % (sign_string, x) elif not is_x_decade: - s = self._non_decade_format(sign_string, base, fx, usetex) + return self._non_decade_format(sign_string, base, fx, usetex) else: - s = r'$\mathdefault{%s%s^{%d}}$' % (sign_string, base, fx) - return self.fix_minus(s) + return r'$\mathdefault{%s%s^{%d}}$' % (sign_string, base, fx) class LogFormatterSciNotation(LogFormatterMathtext): @@ -1308,7 +1307,7 @@ def __call__(self, x, pos=None): s = self._one_minus(self._format_value(1-x, 1-self.locs)) else: s = self._format_value(x, self.locs, sci_notation=False) - return r"$\mathdefault{%s}$" % self.fix_minus(s) + return r"$\mathdefault{%s}$" % s def format_data_short(self, value): # docstring inherited