matplotlib 动态调整绘图大小以适应绘图区域内的文本注解

sbtkgmzw  于 2023-05-01  发布在  其他
关注(0)|答案(1)|浏览(137)

我正在向散点图的点添加文本注解,如下所示:

我事先不知道文本注解的长度。上图显示了文本注解延伸到绘图区域之外。
我想在数据坐标中找到每个文本注解的长度,以便通过设置ylim来动态调整绘图区域中所有文本注解的大小。
我尝试了三种基于以下堆栈溢出答案的解决方案。
我使用两种设置进行了测试:

  • macOS Catalina - 10。15.7
  • Python - 3.8。14
  • matplotlib - 3。3.1

  • Windows 11 - 22H2
  • Python - 3.10.2
  • matplotlib - 3.5.1
import matplotlib.pyplot as plt
from matplotlib.transforms import TransformedBbox, Bbox

def get_text_object_height_1(text_obj, _ax):
    # Based on
    # https://stackoverflow.com/questions/24581194/matplotlib-text-bounding-box-dimensions

    # to get the text bounding box we need to draw the plot
    _fig = _ax.get_figure()
    _fig.canvas.draw()

    # get bounding box of the text in the data coordinates
    bb = text_obj.get_window_extent(renderer=_fig.canvas.get_renderer())
    transform = _ax.transData.inverted()
    trans_box = TransformedBbox(bb, transform)

    return trans_box.height

def get_text_object_height_2(text_obj, _ax):
    # Based on
    # https://stackoverflow.com/a/35419796/2912349
    # https://stackoverflow.com/questions/58854335/how-to-label-y-ticklabels-as-group-category-in-seaborn-clustermap/58915100#58915100

    # Must move the plot to see the last update. When the plot is saved, the last update is included.

    # the figure needs to have been drawn once, otherwise there is no renderer
    plt.ion()
    plt.show()
    plt.pause(1)  # Can make the pause smaller. Kept it lager to see the action.

    # get bounding box of the text in the data coordinates
    bb = text_obj.get_window_extent(renderer=_ax.get_figure().canvas.get_renderer())
    transform = _ax.transData.inverted()
    trans_box = TransformedBbox(bb, transform)

    plt.ioff()

    return trans_box.height

def get_text_object_height_3(text_obj, _ax):
    # Based on
    # https://stackoverflow.com/questions/5320205/matplotlib-text-dimensions

    # to get the text bounding box we need to draw the plot
    _fig = _ax.get_figure()

    # get text bounding box in figure coordinates
    renderer = _fig.canvas.get_renderer()
    bbox_text = text_obj.get_window_extent(renderer=renderer)

    # transform bounding box to data coordinates
    trans_box = Bbox(_ax.transData.inverted().transform(bbox_text))

    return trans_box.height

x = [1, 2, 3]
y = [5, 8, 7]
labels = ['mid length', 'short', 'this is a long label']

fig, ax = plt.subplots(dpi=300, figsize=(5, 3))
ax.scatter(x=x, y=y)

y_lim_max = 0
gap = 0.3

for idx, label in enumerate(labels):
    label_y_start = y[idx] + gap
    txt = ax.text(x[idx], label_y_start, label, rotation='vertical', fontdict=dict(color='black', alpha=1, size=8),
                  transform=ax.transData)

    # Can change to _2, _3 to see how other two methods work
    label_length = get_text_object_height_1(txt, ax)

    label_y_end = label_y_start + label_length
    # Just to show the computed label length
    plt.plot([x[idx], x[idx]], [label_y_start, label_y_end])
    y_lim_max = label_y_end if y_lim_max < label_y_end else y_lim_max

print(f'\t{y_lim_max:8.2f}')
plt.ylim(0, y_lim_max)
plt.tight_layout()

plt.show()
# plt.savefig('scaled_plot.png')

但是,计算的文本注解长度比文本注解短。请注意,我在每个文本注解旁边绘制了一条线,以演示相应文本注解的计算长度。

我做错了什么吗?
是否有一种方法可以获得文本注解的正确尺寸?
是否有不同的方法来调整绘图区域的大小,以包括所有的文本注解?

kx5bkwkv

kx5bkwkv1#

这本身并不是一个答案,而是证明了清洁解决方案的不可能性。
基于特伦顿MaKinney的评论,我创建了以下迭代解决方案:多次重绘该图,直到生成OK图。

import matplotlib.pyplot as plt
from matplotlib.transforms import TransformedBbox

def get_text_object_height_1(text_obj, _ax):
    # Based on
    # https://stackoverflow.com/questions/24581194/matplotlib-text-bounding-box-dimensions

    # to get the text bounding box we need to draw the plot
    _fig = _ax.get_figure()
    _fig.canvas.draw()

    # get bounding box of the text in the data coordinates
    bb = text_obj.get_window_extent(renderer=_fig.canvas.get_renderer())
    transform = _ax.transData.inverted()
    trans_box = TransformedBbox(bb, transform)

    return trans_box.height

def get_text_object_height_2(text_obj, _ax):
    # Based on
    # https://stackoverflow.com/a/35419796/2912349
    # https://stackoverflow.com/questions/58854335/how-to-label-y-ticklabels-as-group-category-in-seaborn-clustermap/58915100#58915100

    # Must move the plot to see the last update. When the plot is saved, the last update is included.

    # the figure needs to have been drawn once, otherwise there is no renderer
    plt.ion()
    plt.show()
    plt.pause(1)  # Can make the pause smaller. Kept it lager to see the action.

    # get bounding box of the text in the data coordinates
    bb = text_obj.get_window_extent(renderer=_ax.get_figure().canvas.get_renderer())
    transform = _ax.transData.inverted()
    trans_box = TransformedBbox(bb, transform)

    plt.ioff()

    return trans_box.height

x = [1, 2, 3]
y = [5, 8, 7]
labels = ['mid length', 'short', 'this is a long label']

y_lim_max_new = max(y)
tolerance = 10E-5
gap = 0.3
keep_adjusting = True

iteration = 0

while keep_adjusting:
    y_lim_max_old = y_lim_max_new

    plt.close()
    fig, ax = plt.subplots(dpi=300, figsize=(5, 3))
    ax.scatter(x=x, y=y)
    ax.set_ylim(0, y_lim_max_new)

    for idx, label in enumerate(labels):
        label_y_start = y[idx] + gap
        txt = ax.text(x[idx], label_y_start, label, rotation='vertical', fontdict=dict(color='black', alpha=1, size=8),
                      transform=ax.transData)

        # To watch intermediate plots as they get scaled, change to get_text_object_height_2
        label_length = get_text_object_height_1(txt, ax)

        label_y_end = label_y_start + label_length
        # Just to show the computed label length
        plt.plot([x[idx], x[idx]], [label_y_start, label_y_end])
        y_lim_max_new = label_y_end if label_y_end > y_lim_max_new else y_lim_max_new

    # Check whether y_lim_max_new has "significantly" changed from the previous value
    keep_adjusting = abs(y_lim_max_new - y_lim_max_old) > tolerance

    iteration += 1
    print(f'\t{iteration:3}.\t{y_lim_max_new:8.10f}\t{y_lim_max_new - y_lim_max_old:8.10f}')

plt.tight_layout()

plt.show()
# plt.savefig('scaled_plot.png')

在我测试代码的系统中,循环迭代了14次,这意味着它为标签提供了14种不同的高度。

我对未来的直觉

坐标系变换矩阵可以取决于xlim和ylim值。我们使用当前的变换矩阵来计算标签的维数,然后更新ylim。这会使我们用来计算标签尺寸的变换矩阵无效,使其在更新后的坐标系中不正确。

相关问题