diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 45e51f53984d..3a8c13b1403d 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2035,6 +2035,7 @@ def __init__(self): p.non_math = Forward() p.operatorname = Forward() p.overline = Forward() + p.overset = Forward() p.placeable = Forward() p.rbrace = Forward() p.rbracket = Forward() @@ -2053,6 +2054,7 @@ def __init__(self): p.symbol = Forward() p.symbol_name = Forward() p.token = Forward() + p.underset = Forward() p.unknown_symbol = Forward() # Set names on everything -- very useful for debugging @@ -2169,6 +2171,18 @@ def __init__(self): - (p.required_group | Error("Expected \\overline{value}")) ) + p.overset <<= Group( + Suppress(Literal(r"\overset")) + - ((p.simple_group + p.simple_group) + | Error("Expected \\overset{body}{annotation}")) + ) + + p.underset <<= Group( + Suppress(Literal(r"\underset")) + - ((p.simple_group + p.simple_group) + | Error("Expected \\underset{body}{annotation}")) + ) + p.unknown_symbol <<= Combine(p.bslash + Regex("[A-Za-z]*")) p.operatorname <<= Group( @@ -2190,6 +2204,8 @@ def __init__(self): | p.dfrac | p.binom | p.genfrac + | p.overset + | p.underset | p.sqrt | p.overline | p.operatorname @@ -2842,6 +2858,38 @@ def binom(self, s, loc, toks): return self._genfrac('(', ')', 0.0, self._MathStyle.TEXTSTYLE, num, den) + def _genset(self, state, body, annotation, overunder): + thickness = state.font_output.get_underline_thickness( + state.font, state.fontsize, state.dpi) + + body.shrink() + + cbody = HCentered([body]) + cannotation = HCentered([annotation]) + width = max(cbody.width, cannotation.width) + cbody.hpack(width, 'exactly') + cannotation.hpack(width, 'exactly') + + vgap = thickness * 3 + if overunder == "under": + vlist = Vlist([cannotation, # annotation + Vbox(0, vgap), # space + cbody # body + ]) + # Shift so the annotation sits in the same vertical position + shift_amount = cannotation.depth + cbody.height + vgap + + vlist.shift_amount = shift_amount + else: + vlist = Vlist([cbody, # body + Vbox(0, vgap), # space + cannotation # annotation + ]) + + # To add horizontal gap between symbols: wrap the Vlist into + # an Hlist and extend it with an Hbox(0, horizontal_gap) + return vlist + def sqrt(self, s, loc, toks): (root, body), = toks state = self.get_state() @@ -2902,6 +2950,24 @@ def overline(self, s, loc, toks): hlist = Hlist([rightside]) return [hlist] + def overset(self, s, loc, toks): + assert len(toks) == 1 + assert len(toks[0]) == 2 + + state = self.get_state() + body, annotation = toks[0] + + return self._genset(state, body, annotation, overunder="over") + + def underset(self, s, loc, toks): + assert len(toks) == 1 + assert len(toks[0]) == 2 + + state = self.get_state() + body, annotation = toks[0] + + return self._genset(state, body, annotation, overunder="under") + def _auto_sized_delimiter(self, front, middle, back): state = self.get_state() if len(middle): diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_dejavusans_01.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_dejavusans_01.png new file mode 100644 index 000000000000..f303fe49e281 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_dejavusans_01.png differ diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 9bd6ca7366dc..16ac7c7bacbd 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -115,6 +115,7 @@ # images. lightweight_math_tests = [ r'$\sqrt[ab]{123}$', # github issue #8665 + r'$x \overset{f}{\rightarrow} \overset{f}{x} \underset{xx}{ff} \overset{xx}{ff} \underset{f}{x} \underset{f}{\leftarrow} x$', # github issue #18241 ] digits = "0123456789" @@ -246,6 +247,8 @@ def test_fontinfo(): (r'$\left($', r'Expected "\right"'), (r'$\dfrac$', r'Expected \dfrac{num}{den}'), (r'$\dfrac{}{}$', r'Expected \dfrac{num}{den}'), + (r'$\overset$', r'Expected \overset{body}{annotation}'), + (r'$\underset$', r'Expected \underset{body}{annotation}'), ], ids=[ 'hspace without value', @@ -266,6 +269,8 @@ def test_fontinfo(): 'unclosed parentheses without sizing', 'dfrac without parameters', 'dfrac with empty parameters', + 'overset without parameters', + 'underset without parameters', ] ) def test_mathtext_exceptions(math, msg):