在 matplotlib 注释中添加一行

Add a line to matplotlib annotations

我有一个 matplotlib 图,其中对某些点进行了注释。我已经弄清楚如何自己做注释,包括箭头和所有内容。但是,我需要在注释文本旁边为每个注释添加一行。它应该 运行 与文本平行,与文本有一定的偏移量(以磅为单位)。线条的长度基于每个注释点所具有的百分比值。理想情况下,我想要一条长度始终相同的行(大约 15 个文本字符,这是注释中文本的最大长度),但根据提到的百分比值,可以说是红色和灰色部分。 非常感谢任何帮助或建议。

编辑: 这是一些模拟数据点的最小示例:

import numpy as np
import matplotlib.pyplot as plt

x=[2, 3, 4, 6, 7, 8, 10, 11]
y=[1, 3, 4, 2, 3, 1, 5, 2]
tx=[3, 4, 5, 6, 7, 8, 9, 10]
yd=dict(zip(x, y))

plt.scatter(x, y)
plt.xlim(0, 14)
plt.ylim(0, 8)

tspace=list(np.linspace(.05, .95, len(tx)))
tsd=dict(zip(tx, tspace))

arpr = {"arrowstyle": "-",
        "connectionstyle": "arc,angleA=-90,armA=20,angleB=90,armB=20,rad=10"}
for i, j in zip(x, tx):
    plt.annotate("foo bar baz", (i, yd[i]), (tsd[j], .75),
              textcoords="axes fraction", arrowprops=arpr,
              annotation_clip=False, rotation="vertical")

这里是当前输出与期望输出的比较:

您可以使用 plt.Rectangle 绘制条形图 — 首先是灰色条形图,它是整个条形图的高度,然后是红色条形图,它是整个条形图高度的百分比。

但是,由于矩形的宽度和长度参数以绘图上的 x 和 y 坐标为单位,我们需要能够访问您所做的文本注释的坐标。

您使用 textcoords="axes fraction" 设置注释坐标,这使得很难在 x 和 y 坐标中访问矩形的起始坐标和结束坐标,因此我为 x_min, x_max, y_min, y_max 定义了一些常量绘图的限制,然后直接从 tspace 列表以及条形注释计算文本注释的坐标。

每个条形的红色百分比 space 可以在列表中设置,这样就可以通用了。

import numpy as np
import matplotlib.pyplot as plt

x=[2, 3, 4, 6, 7, 8, 10, 11]
y=[1, 3, 4, 2, 3, 1, 5, 2]
tx=[3, 4, 5, 6, 7, 8, 9, 10]
yd=dict(zip(x, y))

fig,ax = plt.subplots(1,1)
plt.scatter(x, y)
x_min, x_max = 0, 14
y_min, y_max = 0, 8
y_text_end = 0.75*(y_max-y_min)
plt.xlim(0, 14)
plt.ylim(0, 8)

tspace=list(np.linspace(.05, .95, len(tx)))
# tsd=dict(zip(tx, tspace))

# random percentage values to demonstrate the bar functionality
bar_percentages = [0.95, 0.9, 0.8, 0.6, 0.4, 0.2, 0.1, 0.05]
bar_width = 0.2
bar_height = 1.9

arpr = {"arrowstyle": "-",
        "connectionstyle": "arc,angleA=-90,armA=20,angleB=90,armB=20,rad=10"}

## axes fraction is convenient but it's important to be able to access the exact coordinates for the Rectangle function
for i, x_val in enumerate(x):
    plt.annotate("foo bar baz", (x_val, yd[x_val]), (tspace[i]*(x_max-x_min), y_text_end),
    arrowprops=arpr, annotation_clip=False, rotation="vertical")

    bar_grey = plt.Rectangle((tspace[i]*(x_max-x_min)+0.4, y_text_end-0.1), bar_width, bar_height, fc='#cccccc')
    bar_red = plt.Rectangle((tspace[i]*(x_max-x_min)+0.4, y_text_end-0.1), bar_width, bar_percentages[i]*bar_height, fc='r')
    plt.gca().add_patch(bar_grey)
    plt.gca().add_patch(bar_red)

plt.show()

我已经找到了一个解决方案,虽然是一个 hacky 的解决方案,并且没有理想的“灰盒”,但它对我的目的来说很好,如果它可能对某人有帮助,我会在这里分享它。如果有人知道改进,请随时贡献。感谢@DerekO 提供 ,我将其合并到我的解决方案中。

改编自this matplotlib demo。我只是将自定义框移到了文本之外,并使用百分比的附加参数修改了宽度和高度。我不得不将它分成两个实际的注释,因为箭头不会使用自定义框从正确的位置开始,但这种方式工作正常。 scaling/zooming 现在表现良好并跟随文本。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import BoxStyle

class MyStyle(BoxStyle._Base):
    def __init__(self, pad, per=1.):
        self.pad = pad
        self.per = per
        super().__init__()

    def transmute(self, x0, y0, width, height, mutation_size):
        # padding
        pad = mutation_size * self.pad

        # width and height with padding added.
        width = width + 2.*pad
        width *= self.per
        height = 8.
        # boundary of the padded box
        x0, y0 = x0-pad, y0-pad,
        x1, y1 = x0+width, y0-height

        cp = [(x0, y0),
              (x1, y0),
              (x1, y1),
              (x0, y1),
              (x0, y0)]

        com = [Path.MOVETO,
               Path.LINETO,
               Path.LINETO,
               Path.LINETO,
               Path.CLOSEPOLY]

        path = Path(cp, com)

        return path


# register the custom style
BoxStyle._style_list["percent"] = MyStyle

x=[2, 3, 4, 6, 7, 8, 10, 11]
y=[1, 3, 4, 2, 3, 1, 5, 2]
tx=[3, 4, 5, 6, 7, 8, 9, 10]
yd=dict(zip(x, y))

fig,ax = plt.subplots(1,1)
plt.scatter(x, y)
x_min, x_max = 0, 14
y_min, y_max = 0, 8
y_text_end = 0.75*(y_max-y_min)
plt.xlim(0, 14)
plt.ylim(0, 8)

tspace=list(np.linspace(.05, .95, len(tx)))
# tsd=dict(zip(tx, tspace))

# random percentage values to demonstrate the bar functionality
bar_percentages = [0.95, 0.9, 0.8, 0.6, 0.4, 0.2, 0.1, 0.05]

arpr = {"arrowstyle": "-",
        "connectionstyle": "arc,angleA=-90,armA=20,angleB=90,armB=20,rad=10"}

## axes fraction is convenient but it's important to be able to access the exact coordinates for the Rectangle function
for i, x_val in enumerate(x):
    plt.annotate("", (x_val, yd[x_val]), (tspace[i]*(x_max-x_min), y_text_end),
                 arrowprops=arpr, annotation_clip=False, rotation="vertical",)
    plt.annotate("foo bar baz", (x_val, yd[x_val]), (tspace[i]*(x_max-x_min), y_text_end),
                 annotation_clip=False, rotation="vertical",
                 va="bottom", ha="right",
                 bbox=dict(boxstyle=f"percent,pad=.2,per={bar_percentages[i]}",
                           fc="red",
                           ec="none"))

del BoxStyle._style_list["percent"]

plt.show()