Skip to content

Commit a00ff40

Browse files
committed
Fix confusion between text height and ascent in metrics calculations.
The text height includes both the ascender and the descender, but the logic in _get_layout is that multiline texts should be treated as having an ascent at least as large as "l" and a descent at least as large as "p" (not a height at least as large as "lp" and a descent at least as large as "p") to prevent lines from bumping into each other (see changes to test_text/test_multiline, where the topmost superscript was close to bumping into the "p" descender previously).
1 parent 65fbe95 commit a00ff40

File tree

1 file changed

+30
-31
lines changed

1 file changed

+30
-31
lines changed

lib/matplotlib/text.py

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,22 @@ def _get_textbox(text, renderer):
3737
# called within the _get_textbox. So, it would be better to move this
3838
# function as a method with some refactoring of _get_layout method.
3939

40-
projected_xs = []
41-
projected_ys = []
40+
projected_xys = []
4241

4342
theta = np.deg2rad(text.get_rotation())
4443
tr = Affine2D().rotate(-theta)
4544

46-
_, parts, d = text._get_layout(renderer)
45+
_, parts, _ = text._get_layout(renderer)
4746

48-
for t, wh, x, y in parts:
49-
w, h = wh
50-
51-
xt1, yt1 = tr.transform((x, y))
52-
yt1 -= d
53-
xt2, yt2 = xt1 + w, yt1 + h
54-
55-
projected_xs.extend([xt1, xt2])
56-
projected_ys.extend([yt1, yt2])
47+
for t, (w, a, d), x, y in parts:
48+
xt, yt = tr.transform((x, y))
49+
projected_xys.extend([
50+
(xt, yt + a),
51+
(xt, yt - d),
52+
(xt + w, yt + a),
53+
(xt + w, yt - d),
54+
])
55+
projected_xs, projected_ys = zip(*projected_xys)
5756

5857
xt_box, yt_box = min(projected_xs), min(projected_ys)
5958
w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box
@@ -441,8 +440,10 @@ def _get_layout(self, renderer):
441440
thisx, thisy = 0.0, 0.0
442441
lines = self._get_wrapped_text().split("\n") # Ensures lines is not empty.
443442

444-
ws = []
445-
hs = []
443+
# Reminder: The ascent (a) goes from the baseline to the top and the
444+
# descent (d) from the baseline to the bottom; both are (typically)
445+
# nonnegative. The height h is the sum, h = a + d.
446+
wads = [] # (width, ascents, descents)
446447
xs = []
447448
ys = []
448449

@@ -451,7 +452,8 @@ def _get_layout(self, renderer):
451452
renderer, "lp", self._fontproperties,
452453
ismath="TeX" if self.get_usetex() else False,
453454
dpi=self.get_figure(root=True).dpi)
454-
min_dy = (lp_h - lp_d) * self._linespacing
455+
lp_a = lp_h - lp_d
456+
min_dy = lp_a * self._linespacing
455457

456458
for i, line in enumerate(lines):
457459
clean_line, ismath = self._preprocess_math(line)
@@ -462,25 +464,21 @@ def _get_layout(self, renderer):
462464
else:
463465
w = h = d = 0
464466

465-
# For multiline text, increase the line spacing when the text
466-
# net-height (excluding baseline) is larger than that of a "l"
467-
# (e.g., use of superscripts), which seems what TeX does.
468-
h = max(h, lp_h)
467+
a = h - d
468+
# To ensure good linespacing, pretend that the ascent (resp.
469+
# descent) of all lines is at least as large as "l" (resp. "p").
470+
a = max(a, lp_a)
469471
d = max(d, lp_d)
470472

471-
ws.append(w)
472-
hs.append(h)
473-
474473
# Metrics of the last line that are needed later:
475-
baseline = (h - d) - thisy
474+
baseline = a - thisy
476475

477-
if i == 0:
478-
# position at baseline
479-
thisy = -(h - d)
480-
else:
481-
# put baseline a good distance from bottom of previous line
482-
thisy -= max(min_dy, (h - d) * self._linespacing)
476+
if i == 0: # position at baseline
477+
thisy = -a
478+
else: # put baseline a good distance from bottom of previous line
479+
thisy -= max(min_dy, a * self._linespacing)
483480

481+
wads.append((w, a, d))
484482
xs.append(thisx) # == 0.
485483
ys.append(thisy)
486484

@@ -490,6 +488,7 @@ def _get_layout(self, renderer):
490488
descent = d
491489

492490
# Bounding box definition:
491+
ws = [w for w, a, d in wads]
493492
width = max(ws)
494493
xmin = 0
495494
xmax = width
@@ -587,7 +586,7 @@ def _get_layout(self, renderer):
587586
# now rotate the positions around the first (x, y) position
588587
xys = M.transform(offset_layout) - (offsetx, offsety)
589588

590-
return bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent
589+
return bbox, list(zip(lines, wads, *xys.T)), descent
591590

592591
def set_bbox(self, rectprops):
593592
"""
@@ -876,7 +875,7 @@ def draw(self, renderer):
876875

877876
angle = self.get_rotation()
878877

879-
for line, wh, x, y in info:
878+
for line, wad, x, y in info:
880879

881880
mtext = self if len(info) == 1 else None
882881
x = x + posx

0 commit comments

Comments
 (0)