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

如何获取Matplotlib Text对象的旋转锚点实现刻度标签等文本自动缩放

Matplotlib旋转Text对象锚点获取及文本重叠检测问题

我正在尝试自动缩放Matplotlib绘图中的Text对象(本文以刻度标签为核心研究对象),避免文本出现重叠,该能力用于一个CLI程序的原型绘图模块,该模块需要动态输出多种不同类型、不同尺寸的图表。

当前实现方案中,我需要获取任意旋转的matplotlib.text.Text bounding box的旋转锚点,核心问题为:如何获取Text(...).get_window_extent()返回的Bbox对应的旋转锚点坐标,最好坐标单位与Bbox返回的坐标系一致?

为避免出现XY问题,我补充更多背景信息:
plt.tight_layout()和constrained_layout无法覆盖所有Text对象的缩放场景,为每一种图表类型推导鲁棒的文本缩放启发式规则可行性极低,我暂时接受通过重绘来检测文本重叠的方案。

问题演示动画:
刻度标签重缩放动画示例

如果Text对象被旋转,其返回的Bbox是覆盖文本范围的轴对齐矩形,和动画中绘制的红/绿框不同,该矩形无法代表文本实际覆盖的区域,因此直接调用Bbox.intersection()判断重叠的效果不符合预期。
我希望使用红/绿框的范围来判断重叠,但这些框是通过Text.set_bbox绘制的,获取其范围仍然会得到非旋转的类正方形形状,我无法找到其初始旋转原点。

此前参照相关技术社区的建议,尝试使用shapely库的Polygon对象处理,因为此前我尝试用Matplotlib的transform等相关接口实现时一直没有成功。
但我当前的实现方案复杂度较高,流程如下:

  1. 重缩放前存储刻度标签的旋转角度
  2. 获取刻度标签的Bbox范围
  3. 将标签旋转角度设为0,获取其未旋转状态下的bounding box顶点
  4. 将原始Bbox的角点/顶点转换为shapely库的Polygon对象,该库支持计算旋转多边形的相交区域
  5. 将刻度标签恢复为原始旋转角度
  6. 根据文本的水平/垂直对齐属性估算旋转锚点/原点,该估算结果误差较大
  7. 将Polygon对象围绕估算的旋转原点/锚点旋转
  8. 计算旋转后多边形的相交/重叠区域
  9. 如果没有重叠或者达到迭代上限则停止,否则根据上一次迭代的重叠面积差值等参数缩小字号,重绘图表/文本对象后回到步骤1

实现上述逻辑的代码耦合度较高,长度过长不便贴出,因此我附上制作上述演示动画的代码片段(该代码也没有正确处理旋转逻辑,可以代表当前存在的问题)。

目前我希望得到两种方案中的任意一种:要么找到获取Text对象精确旋转锚点的方法,优化上述流程的第6步;要么找到更简单的碰撞检测方案,替代当前复杂的实现。

更新:
我正在查阅Matplotlib源码,寻找旋转锚点的内部计算逻辑,看起来该逻辑在matplotlib.text.Text._get_layout()中实现(Matplotlib 3.4.3版本从388行开始),该方法中通过未旋转框的角点计算偏移量,这可能对实现有帮助,但该方法还使用了一个我还未理解含义的'baseline'参数。

示例代码如下:

#!/usr/bin/env python3
from __future__ import annotations

import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.axes import Axes
from matplotlib.text import Annotation
from matplotlib.transforms import Bbox

N_DATAPOINTS: int = 25
INITIAL_LABELSIZE: float = 10
N_FRAMES: int = 60


def calculate_overlap_area_bboxes(bbox_a: Bbox, bbox_b: Bbox) -> float:
    overlap_bbox: Bbox = Bbox.intersection(bbox_a, bbox_b)

    if overlap_bbox is None:
        intersecting_area = 0
    else:
        intersecting_area: float = \
            (overlap_bbox.xmax - overlap_bbox.xmin) * (overlap_bbox.ymax - overlap_bbox.ymin)

    return intersecting_area


def labelsize_update(ii: int,
                     ax: Axes,
                     area_annot: Annotation, overlap_annot: Annotation,
                     delta_fontsz: float = .25):
    # Get first ticklabels and their bounding boxes
    lbl_a, lbl_b = ax.get_xticklabels()[:2]
    first_bbox = lbl_a.get_window_extent().transformed(ax.transData.inverted())
    second_bbox = lbl_b.get_window_extent().transformed(ax.transData.inverted())
    
    # Check overlap
    overlap = calculate_overlap_area_bboxes(bbox_a=first_bbox, bbox_b=second_bbox)
    cur_ticklabels = ax.get_xticklabels()

    # There are of course more efficient ways to converge to a non-overlapping fontsize, but to keep
    # this example short-ish I'll simply make it a bit smaller every iteration as long as there is overlap.
    new_fontsize = cur_ticklabels[0].get_fontsize() - (delta_fontsz if overlap else 0)

    overlap_annot.set_text(
            '\n\n'.join([f"Visual overlap: {'YES' if new_fontsize > 5 else 'NO'}",
                         f"Overlap according to ticklabel Bbox extents: {'YES' if overlap else 'NO'}"]))

    area_annot.set_text(
            '\n'.join([f"Fontsize {new_fontsize: .2f}", f"Overlapping label area: {overlap:.5f}", "(square data units)"]))

    # Update ticklabel sizes
    for label in cur_ticklabels:
        # I can color its rotated bbox for this example, but don't know how to get its coordinates? :(
        label.set_bbox(dict(facecolor='w', edgecolor='r' if new_fontsize > 5 else 'g', alpha=.5, pad=-.1))
        label.set_fontsize(new_fontsize) if ii % N_FRAMES else label.set_fontsize(INITIAL_LABELSIZE)
    
    plt.tight_layout()
    return cur_ticklabels, area_annot, overlap_annot


def main():
    mpl.use("TkAgg")  # Force tkinter backend to allow single way of fixing interactive window size for sake of this example.

    # Prepare initial plot and annotations
    fig, ax = plt.subplots(figsize=(3, 2), dpi=250)
    ax.plot(range(N_DATAPOINTS), [1] * N_DATAPOINTS, alpha=0)
    ax.set_xticks(range(N_DATAPOINTS))
    ax.set_xticklabels([f"{ii} Lorem Ipsum" for ii in range(N_DATAPOINTS)],
                       rotation=45, rotation_mode='anchor', ha='right',
                       fontsize=INITIAL_LABELSIZE)
    ax.get_yaxis().set_visible(False)
    plt.tight_layout()

    area_annot = ax.annotate(f"Fontsize {INITIAL_LABELSIZE}. Overlapping label area: unknown",
                             xy=(.05, .1), xycoords=ax.transAxes,
                             fontsize=4, color='purple')
    overlap_annot = ax.annotate(f"Visual overlap: YES\n\nOverlap according to ticklabel Bbox extents: YES",
                                xy=(.5, .5), xycoords=ax.transAxes,
                                fontsize=5, fontweight='bold', ha='center')

    # Pass everything to animation and start it.
    _ = animation.FuncAnimation(fig, labelsize_update,
                                fargs=[ax, area_annot, overlap_annot],
                                interval=100, blit=False, repeat=True, frames=N_FRAMES)

    # Disable window resizing for the sake of this example.
    plt.get_current_fig_manager().window.resizable(False, False)
    plt.show()


if __name__ == '__main__':
    main()

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 06:24:02