Skip to content

Commit 60f2310

Browse files
anntzerQuLogic
authored andcommitted
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 a6c2501 commit 60f2310

File tree

2 files changed

+43
-42
lines changed

2 files changed

+43
-42
lines changed

lib/matplotlib/offsetbox.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -799,23 +799,24 @@ def get_bbox(self, renderer):
799799
ismath="TeX" if self._text.get_usetex() else False,
800800
dpi=self.get_figure(root=True).dpi)
801801

802-
bbox, info, yd = self._text._get_layout(renderer)
802+
bbox, info = self._text._get_layout(renderer)
803+
_last_line, (_last_width, _last_ascent, last_descent), _last_xy = info[-1]
803804
w, h = bbox.size
804805

805806
self._baseline_transform.clear()
806807

807808
if len(info) > 1 and self._multilinebaseline:
808809
yd_new = 0.5 * h - 0.5 * (h_ - d_)
809-
self._baseline_transform.translate(0, yd - yd_new)
810-
yd = yd_new
810+
self._baseline_transform.translate(0, last_descent - yd_new)
811+
last_descent = yd_new
811812
else: # single line
812-
h_d = max(h_ - d_, h - yd)
813-
h = h_d + yd
813+
h_d = max(h_ - d_, h - last_descent)
814+
h = h_d + last_descent
814815

815816
ha = self._text.get_horizontalalignment()
816817
x0 = {"left": 0, "center": -w / 2, "right": -w}[ha]
817818

818-
return Bbox.from_bounds(x0, -yd, w, h)
819+
return Bbox.from_bounds(x0, -last_descent, w, h)
819820

820821
def draw(self, renderer):
821822
# docstring inherited

lib/matplotlib/text.py

Lines changed: 36 additions & 36 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), xy in parts:
48+
xt, yt = tr.transform(xy)
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
@@ -434,15 +433,18 @@ def update_from(self, other):
434433

435434
def _get_layout(self, renderer):
436435
"""
437-
Return the extent (bbox) of the text together with
438-
multiple-alignment information. Note that it returns an extent
439-
of a rotated text when necessary.
436+
Return
437+
438+
- the (rotated) text bbox, and
439+
- a list of ``(line, (width, ascent, descent), xy)`` tuples for each line.
440440
"""
441441
thisx, thisy = 0.0, 0.0
442442
lines = self._get_wrapped_text().split("\n") # Ensures lines is not empty.
443443

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

@@ -451,7 +453,8 @@ def _get_layout(self, renderer):
451453
renderer, "lp", self._fontproperties,
452454
ismath="TeX" if self.get_usetex() else False,
453455
dpi=self.get_figure(root=True).dpi)
454-
min_dy = (lp_h - lp_d) * self._linespacing
456+
lp_a = lp_h - lp_d
457+
min_dy = lp_a * self._linespacing
455458

456459
for i, line in enumerate(lines):
457460
clean_line, ismath = self._preprocess_math(line)
@@ -462,25 +465,21 @@ def _get_layout(self, renderer):
462465
else:
463466
w = h = d = 0
464467

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)
468+
a = h - d
469+
# To ensure good linespacing, pretend that the ascent (resp.
470+
# descent) of all lines is at least as large as "l" (resp. "p").
471+
a = max(a, lp_a)
469472
d = max(d, lp_d)
470473

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

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)
477+
if i == 0: # position at baseline
478+
thisy = -a
479+
else: # put baseline a good distance from bottom of previous line
480+
thisy -= max(min_dy, a * self._linespacing)
483481

482+
wads.append((w, a, d))
484483
xs.append(thisx) # == 0.
485484
ys.append(thisy)
486485

@@ -490,6 +489,7 @@ def _get_layout(self, renderer):
490489
descent = d
491490

492491
# Bounding box definition:
492+
ws = [w for w, a, d in wads]
493493
width = max(ws)
494494
xmin = 0
495495
xmax = width
@@ -587,7 +587,7 @@ def _get_layout(self, renderer):
587587
# now rotate the positions around the first (x, y) position
588588
xys = M.transform(offset_layout) - (offsetx, offsety)
589589

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

592592
def set_bbox(self, rectprops):
593593
"""
@@ -840,7 +840,7 @@ def draw(self, renderer):
840840
renderer.open_group('text', self.get_gid())
841841

842842
with self._cm_set(text=self._get_wrapped_text()):
843-
bbox, info, descent = self._get_layout(renderer)
843+
bbox, info = self._get_layout(renderer)
844844
trans = self.get_transform()
845845

846846
# don't use self.get_position here, which refers to text
@@ -876,7 +876,7 @@ def draw(self, renderer):
876876

877877
angle = self.get_rotation()
878878

879-
for line, wh, x, y in info:
879+
for line, wad, (x, y) in info:
880880

881881
mtext = self if len(info) == 1 else None
882882
x = x + posx
@@ -1064,7 +1064,7 @@ def get_window_extent(self, renderer=None, dpi=None):
10641064
"want to call 'figure.draw_without_rendering()' first.")
10651065

10661066
with cbook._setattr_cm(fig, dpi=dpi):
1067-
bbox, info, descent = self._get_layout(self._renderer)
1067+
bbox, _ = self._get_layout(self._renderer)
10681068
x, y = self.get_unitless_position()
10691069
x, y = self.get_transform().transform((x, y))
10701070
bbox = bbox.translated(x, y)

0 commit comments

Comments
 (0)