Skip to content

Implement TeX's fraction alignment for Mathtext fractions #22852

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
192 changes: 150 additions & 42 deletions lib/matplotlib/_mathtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,16 +249,11 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi):
)

def get_xheight(self, fontname, fontsize, dpi):
font = self._get_font(fontname)
font.set_size(fontsize, dpi)
pclt = font.get_sfnt_table('pclt')
if pclt is None:
# Some fonts don't store the xHeight, so we do a poor man's xHeight
metrics = self.get_metrics(
fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi)
return metrics.iceberg
xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0)
return xHeight
# Some fonts report the wrong x-height, while some don't store it, so
# we do a poor man's x-height.
metrics = self.get_metrics(
fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi)
return metrics.iceberg

def get_underline_thickness(self, font, fontsize, dpi):
# This function used to grab underline thickness from the font
Expand Down Expand Up @@ -759,16 +754,44 @@ class FontConstantsBase:
# integrals
delta_integral = 0.1

# Percentage of x-height the numerator is shifted up in display style.
num1 = 1.4

# Percentage of x-height the numerator is shifted up in text, script and
# scriptscript styles if there is a fraction line.
num2 = 1.5

# Percentage of x-height the numerator is shifted up in text, script and
# scriptscript styles if there is no fraction line.
num3 = 1.3

# Percentage of x-height the denominator is shifted down in display style.
denom1 = 1.3

# Percentage of x-height the denominator is shifted down in text, script
# and scriptscript styles.
denom2 = 1.1


class ComputerModernFontConstants(FontConstantsBase):
script_space = 0.075
subdrop = 0.2
sup1 = 0.45
sub1 = 0.2
sub2 = 0.3
delta = 0.075
# Previously, the x-height of Computer Modern was obtained from the font
# table. However, that x-height was greater than the the actual (rendered)
# x-height by a factor of 1.771484375 (at font size 12, DPI 100 and hinting
# type 32). Now that we're using the rendered x-height, some font constants
# have been increased by the same factor to compensate.
script_space = 0.132861328125
subdrop = 0.354296875
sup1 = 0.79716796875
sub1 = 0.354296875
sub2 = 0.5314453125
delta = 0.132861328125
delta_slanted = 0.3
delta_integral = 0.3
num1 = 1.5
num2 = 1.5
num3 = 1.5
denom1 = 1.6
denom2 = 1.2


class STIXFontConstants(FontConstantsBase):
Expand All @@ -778,17 +801,27 @@ class STIXFontConstants(FontConstantsBase):
delta = 0.05
delta_slanted = 0.3
delta_integral = 0.3
num1 = 1.6
num2 = 1.6
num3 = 1.6
denom1 = 1.6


class STIXSansFontConstants(FontConstantsBase):
script_space = 0.05
sup1 = 0.8
delta_slanted = 0.6
delta_integral = 0.3
num1 = 1.5
num3 = 1.5
denom1 = 1.5


class DejaVuSerifFontConstants(FontConstantsBase):
pass
num1 = 1.5
num2 = 1.6
num3 = 1.4
denom1 = 1.4


class DejaVuSansFontConstants(FontConstantsBase):
Expand Down Expand Up @@ -1832,6 +1865,7 @@ def set_names_and_parse_actions():
| p.underset
| p.sqrt
| p.overline
| p.auto_delim
)

p.simple <<= (
Expand Down Expand Up @@ -2156,6 +2190,19 @@ def is_slanted(self, nucleus):
def is_between_brackets(self, s, loc):
return False

def is_char_node(self, node):
# TeX defines a `char_node` as one which represents a single character,
# but also states that a `char_node` will never appear in a `Vlist`
# (node134). Further, nuclei made of one `Char` and nuclei made of
# multiple `Char`s have their superscripts and subscripts shifted by
# the same amount. In order to make Mathtext behave similarly, just
# check whether this node is a `Vlist` or has any `Vlist` descendants.
if isinstance(node, Vlist):
return False
if not isinstance(node, Hlist):
return True
return all(map(self.is_char_node, node.children))

def subsuper(self, s, loc, toks):
nucleus = toks.get("nucleus", Hbox(0))
subsuper = toks.get("subsuper", [])
Expand Down Expand Up @@ -2269,36 +2316,56 @@ def subsuper(self, s, loc, toks):
else:
subkern = 0

# Set the minimum shifts for the superscript and subscript (node756).
if self.is_char_node(nucleus):
shift_up = 0
shift_down = 0
else:
drop_amount = constants.subdrop * xHeight * SHRINK_FACTOR
shift_up = nucleus.height - drop_amount
shift_down = nucleus.depth + drop_amount

if super is None:
# node757
# Align subscript without superscript (node757).
x = Hlist([Kern(subkern), sub])
x.shrink()
if self.is_dropsub(last_char):
shift_down = lc_baseline + constants.subdrop * xHeight
else:
shift_down = constants.sub1 * xHeight
shift_down = max(shift_down,
constants.sub1 * xHeight,
x.height - xHeight * 4 / 5)
x.shift_amount = shift_down
else:
# Align superscript (node758).
x = Hlist([Kern(superkern), super])
x.shrink()
if self.is_dropsub(last_char):
shift_up = lc_height - constants.subdrop * xHeight
else:
shift_up = constants.sup1 * xHeight
shift_up = max(shift_up,
constants.sup1 * xHeight,
x.depth + xHeight / 4)
if sub is None:
x.shift_amount = -shift_up
else: # Both sub and superscript
else:
# Align subscript with superscript (node759).
y = Hlist([Kern(subkern), sub])
y.shrink()
if self.is_dropsub(last_char):
shift_down = lc_baseline + constants.subdrop * xHeight
else:
shift_down = constants.sub2 * xHeight
# If sub and superscript collide, move super up
clr = (2.0 * rule_thickness -
shift_down = max(shift_down, constants.sub2 * xHeight)
# If the subscript and superscript are too close to each other,
# move the subscript down.
clr = (4 * rule_thickness -
((shift_up - x.depth) - (y.height - shift_down)))
if clr > 0.:
shift_up += clr
shift_down += clr
clr = xHeight * 4 / 5 - shift_up + x.depth
if clr > 0:
shift_up += clr
shift_down -= clr
x = Vlist([
x,
Kern((shift_up - x.depth) - (y.height - shift_down)),
Expand All @@ -2322,32 +2389,73 @@ def _genfrac(self, ldelim, rdelim, rule, style, num, den):
state = self.get_state()
thickness = state.get_current_underline_thickness()

# The fraction line (if present) must be aligned with the minus sign.
# Therefore, the height of the latter from the baseline is the axis
# height.
metrics = state.font_output.get_metrics(
state.font, mpl.rcParams['mathtext.default'],
'\u2212', state.fontsize, state.dpi)
axis_height = (metrics.ymax + metrics.ymin) / 2
constants = _get_font_constant_set(state)
xHeight = state.font_output.get_xheight(
state.font, state.fontsize, state.dpi)

for _ in range(style.value):
xHeight *= SHRINK_FACTOR
num.shrink()
den.shrink()
cnum = HCentered([num])
cden = HCentered([den])
width = max(num.width, den.width)
cnum.hpack(width, 'exactly')
cden.hpack(width, 'exactly')
vlist = Vlist([cnum, # numerator
Vbox(0, thickness * 2.0), # space
Hrule(state, rule), # rule
Vbox(0, thickness * 2.0), # space
cden # denominator
])

# Shift so the fraction line sits in the middle of the
# equals sign
metrics = state.font_output.get_metrics(
state.font, mpl.rcParams['mathtext.default'],
'=', state.fontsize, state.dpi)
shift = (cden.height -
((metrics.ymax + metrics.ymin) / 2 -
thickness * 3.0))
vlist.shift_amount = shift

result = [Hlist([vlist, Hbox(thickness * 2.)])]
# Align the fraction with a fraction line (node743, node744 and
# node746).
if rule:
if style is self._MathStyle.DISPLAYSTYLE:
num_shift_up = constants.num1 * xHeight
den_shift_down = constants.denom1 * xHeight
min_clr = 3 * rule
else:
num_shift_up = constants.num2 * xHeight
den_shift_down = constants.denom2 * xHeight
min_clr = rule
num_clr = max(
num_shift_up - cnum.depth - axis_height - rule / 2, min_clr)
den_clr = max(
axis_height - rule / 2 + den_shift_down - cden.height, min_clr)
# Possible bug in fraction rendering. See GitHub PR 22852 comments.
vlist = Vlist([cnum, # numerator
Vbox(0, num_clr - rule), # space
Hrule(state, rule), # rule
Vbox(0, den_clr + rule), # space
cden # denominator
])
vlist.shift_amount = cden.height + den_clr + rule / 2 - axis_height

# Align the fraction without a fraction line (node743, node744 and
# node745).
else:
if style is self._MathStyle.DISPLAYSTYLE:
num_shift_up = constants.num1 * xHeight
den_shift_down = constants.denom1 * xHeight
min_clr = 7 * thickness
else:
num_shift_up = constants.num3 * xHeight
den_shift_down = constants.denom2 * xHeight
min_clr = 3 * thickness
def_clr = num_shift_up - cnum.depth + den_shift_down - cden.height
clr = max(def_clr, min_clr)
vlist = Vlist([cnum, # numerator
Vbox(0, clr), # space
cden # denominator
])
vlist.shift_amount = den_shift_down
if def_clr < min_clr:
vlist.shift_amount += (min_clr - def_clr) / 2

result = [Hlist([Hbox(thickness), vlist, Hbox(thickness)])]
if ldelim or rdelim:
if ldelim == '':
ldelim = '.'
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading