Home > Software engineering >  Auto-fitting text into boxes while allowing word-wrapping
Auto-fitting text into boxes while allowing word-wrapping

Time:01-05

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: IMAGE: 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()

enter image description here

  • Related