Problem statement
I'm trying to fit text to a bounding box, similar to this question.
As text is getting longer, word-wrapping is required so that not all text is on one line and the font size is big enough for the text to be legible.
What I tried
I modified the accepted answer to the question above. A MWE is shown below.
from typing import Optional, Literal
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
from matplotlib.text import Annotation
from matplotlib.transforms import Transform, Bbox
from matplotlib.figure import Figure
def text_with_autofit(
ax: plt.Axes,
txt: str,
xy: tuple[float, float],
width: float,
height: float,
*,
transform: Optional[Transform] = None,
ha: Literal["left", "center", "right"] = "center",
va: Literal["bottom", "center", "top"] = "center",
show_rect: bool = False,
**kwargs,
):
if transform is None:
transform = ax.transData
# Different alignments give different bottom left and top right anchors.
x, y = xy
xa0, xa1 = {
"center": (x - width / 2, x width / 2),
"left": (x, x width),
"right": (x - width, x),
}[ha]
ya0, ya1 = {
"center": (y - height / 2, y height / 2),
"bottom": (y, y height),
"top": (y - height, y),
}[va]
a0 = xa0, ya0
a1 = xa1, ya1
x0, y0 = transform.transform(a0)
x1, y1 = transform.transform(a1)
# rectangle region size to constrain the text in pixel
rect_width = x1 - x0
rect_height = y1 - y0
fig: Figure = ax.get_figure()
dpi = fig.dpi
rect_height_inch = rect_height / dpi
# Initial fontsize according to the height of boxes
fontsize = rect_height_inch * 72
text: Annotation = ax.annotate(txt, xy, ha=ha, va=va, xycoords=transform, **kwargs)
# Adjust the fontsize according to the box size.
text.set_fontsize(fontsize)
bbox: Bbox = text.get_window_extent(fig.canvas.get_renderer())
adjusted_size = fontsize * rect_width / bbox.width
text.set_fontsize(adjusted_size)
if show_rect:
rect = mpatches.Rectangle(a0, width, height, fill=False, ls="--")
ax.add_patch(rect)
return text
def main() -> None:
fig, ax = plt.subplots(2, 1)
# In the box with the width of 0.4 and the height of 0.4 at (0.5, 0.5), add the text.
text_with_autofit(
ax[0], "Hello, World! How are you?", (0.5, 0.5), 0.4, 0.4, show_rect=True
)
# In the box with the width of 0.6 and the height of 0.4 at (0.5, 0.5), add the text.
text_with_autofit(
ax[1],
"Hello, World! How are you? I'm actually a much longer text block, i.e. my width is longer than the other text block.",
(0.5, 0.5),
0.4,
0.4,
show_rect=True,
wrap=True,
)
plt.show()
Above code produces this output:
What I expect
I expected the text in the bottom subfigure to wrap and have larger fontsize. Instead, wrapping seems to be ignored.
How to modify the MWE to incorporate text wrapping?
CodePudding user response:
First you'll need to determine your minimum acceptable font size (subjective) then you'll need to iterate to determine how many lines your text will wrap to. The textwrap
module comes in handy here.
from typing import Optional, Literal
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
from matplotlib.text import Annotation
from matplotlib.transforms import Transform, Bbox
from matplotlib.figure import Figure
import textwrap
def text_with_autofit(
ax: plt.Axes,
txt: str,
xy: tuple[float, float],
width: float,
height: float,
*,
min_font_size = None,
transform: Optional[Transform] = None,
ha: Literal["left", "center", "right"] = "center",
va: Literal["bottom", "center", "top"] = "center",
show_rect: bool = False,
**kwargs,
):
if transform is None:
transform = ax.transData
# Different alignments give different bottom left and top right anchors.
x, y = xy
xa0, xa1 = {
"center": (x - width / 2, x width / 2),
"left": (x, x width),
"right": (x - width, x),
}[ha]
ya0, ya1 = {
"center": (y - height / 2, y height / 2),
"bottom": (y, y height),
"top": (y - height, y),
}[va]
a0 = xa0, ya0
a1 = xa1, ya1
x0, y0 = transform.transform(a0)
x1, y1 = transform.transform(a1)
# rectangle region size to constrain the text in pixel
rect_width = x1 - x0
rect_height = y1 - y0
fig: Figure = ax.get_figure()
dpi = fig.dpi
rect_height_inch = rect_height / dpi
# Initial fontsize according to the height of boxes
fontsize = rect_height_inch * 72
wrap_lines = 1
while True:
wrapped_txt = '\n'.join(textwrap.wrap(txt, width=len(txt)//wrap_lines))
text: Annotation = ax.annotate(wrapped_txt, xy, ha=ha, va=va, xycoords=transform, **kwargs)
text.set_fontsize(fontsize)
# Adjust the fontsize according to the box size.
bbox: Bbox = text.get_window_extent(fig.canvas.get_renderer())
adjusted_size = fontsize * rect_width / bbox.width
if min_font_size is None or adjusted_size >= min_font_size:
break
text.remove()
wrap_lines = 1
text.set_fontsize(adjusted_size)
if show_rect:
rect = mpatches.Rectangle(a0, width, height, fill=False, ls="--")
ax.add_patch(rect)
return text
def main() -> None:
fig, ax = plt.subplots(2, 1)
# In the box with the width of 0.4 and the height of 0.4 at (0.5, 0.5), add the text.
text_with_autofit(
ax[0], "Hello, World! How are you?", (0.5, 0.5), 0.4, 0.4, show_rect=True
)
# In the box with the width of 0.6 and the height of 0.4 at (0.5, 0.5), add the text.
text_with_autofit(
ax[1],
"Hello, World! How are you? I'm actually a much longer text block, i.e. my width is longer than the other text block.",
(0.5, 0.5),
0.4,
0.4,
min_font_size=6,
show_rect=True,
wrap=True,
)
plt.show()