You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改Matplotlib代码实现文本适配方框并支持自动换行?

问题描述

我尝试实现文本适配到指定边界框的功能,需求是当文本较长时自动换行,避免挤在一行,同时保证字号足够大以保证可读性。

我的尝试

我参考了相关问题的已采纳答案并修改,以下是最小可复现示例(MWE):

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

    # 不同对齐方式对应不同的锚点坐标
    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)
    # 转换为像素单位的边界框尺寸
    rect_width = x1 - x0
    rect_height = y1 - y0

    fig: Figure = ax.get_figure()
    dpi = fig.dpi
    rect_height_inch = rect_height / dpi
    # 按边界框高度初始化字号
    fontsize = rect_height_inch * 72

    text: Annotation = ax.annotate(txt, xy, ha=ha, va=va, xycoords=transform, **kwargs)

    # 根据边界框宽度调整字号
    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)

    # 在(0.5,0.5)位置的0.4x0.4边界框中添加文本
    text_with_autofit(
        ax[0], "Hello, World! How are you?", (0.5, 0.5), 0.4, 0.4, show_rect=True
    )

    # 在同一位置的0.4x0.4边界框中添加长文本
    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()

上述代码生成如下输出:
代码输出效果

预期效果

我期望下方子图中的文本能够自动换行,并且字号更大,但目前换行功能似乎被忽略了。如何修改该MWE以实现文本自动换行?


解决方案

问题核心是ax.annotate的wrap参数不会自动关联边界框宽度,且原有字号调整逻辑仅考虑单行文本宽度,未兼顾换行后的高度约束。修改如下:

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

    # 计算边界框锚点坐标
    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)
    rect_width_pix = x1 - x0
    rect_height_pix = y1 - y0

    fig: Figure = ax.get_figure()
    dpi = fig.dpi
    renderer = fig.canvas.get_renderer()

    # 创建文本对象
    text: Annotation = ax.annotate(txt, xy, ha=ha, va=va, xycoords=transform, **kwargs)

    # 按边界框高度初始化字号
    fontsize = (rect_height_pix / dpi) * 72
    text.set_fontsize(fontsize)

    # 手动指定换行宽度,激活wrap功能
    text._get_wrap_line_width = lambda: rect_width_pix

    # 迭代调整字号,确保文本完全适配边界框
    max_iter = 10
    for _ in range(max_iter):
        bbox: Bbox = text.get_window_extent(renderer)
        # 取宽度和高度缩放比例的最小值,避免超出边界
        scale_width = rect_width_pix / bbox.width
        scale_height = rect_height_pix / bbox.height
        scale = min(scale_width, scale_height)

        if abs(scale - 1) < 0.01:  # 误差足够小时停止迭代
            break

        fontsize *= scale
        text.set_fontsize(fontsize)

    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)

    text_with_autofit(
        ax[0], "Hello, World! How are you?", (0.5, 0.5), 0.4, 0.4, show_rect=True
    )

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

关键修改说明

  1. 激活自动换行:通过text._get_wrap_line_width手动指定换行宽度为边界框的像素宽度,让wrap=True参数真正生效。
  2. 双向约束字号:同时对比文本的宽高与边界框的宽高,取最小缩放比例调整字号,确保文本既不超出宽度,也不超出高度,同时保证字号最大化。
  3. 迭代优化:加入循环迭代调整,缩小文本尺寸与边界框的误差,避免单次调整的偏差。

内容的提问来源于stack exchange,提问作者Siem

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 20:20:23