python - 相对于文本而不是边界框对齐任意旋转的文本注释

标签 python python-3.x matplotlib

在尝试回答 old, unanswered question 时, 我在 matplotlib 中遇到了一个关于文本注释的小问题:在某个位置向图形添加旋转文本时,文本相对于文本的边界框对齐,而不是(假想的)旋转包含文本本身的框。这可能最好用一个小例子来解释: example of text alignment in matplotlib

图中显示了具有不同旋转角度和不同对齐选项的文本片段。对于每个文本对象,红点表示提供给 ax.text() 函数的坐标。蓝色框是文本周围的旋转框,黑色框是文本的近似边界框(有点太大了,但应该明白了)。很容易看出,对于在边缘(左、右、上、下)对齐的情况,红点位于边界框的侧面或边缘,而不是文本框。如果水平和垂直对齐方式都设置为“居中”,则唯一的对齐选项(文本以直观的方式对齐)。现在,这不是错误,而是概述的预期行为 here .然而,在某些情况下,这不是很实用,因为必须“手动”调整位置才能使文本位于所需位置,并且如果旋转角度发生变化或图形重新缩放,则此调整也会发生变化。

问题是,是否有一种可靠的方法来生成与文本框而不是边界框对齐的文本。我已经找到了问题的解决方案,但弄清楚它非常乏味,所以我想我会分享它。

最佳答案

新解决方案rotation_mode="anchor"

matplotlib.text.Text 实际上有一个参数 rotation_mode,它精确地引导请求的功能。默认值是 rotation_mode="default",它从问题中重新创建不需要的行为,而 rotation_mode="anchor" 锚定旋转点根据到文本本身而不是它的边界框。

ax.text(x,y,'test', rotation = deg, rotation_mode="anchor")

另见 the demo_text_rotation_mode example .

有了这个,问题中的例子可以很容易地创建,而不需要继承 Text

from matplotlib import pyplot as plt
import numpy as np

fig, axes = plt.subplots(3,3, figsize=(10,10),dpi=100)
aligns = [ (va,ha) for va in ('top', 'center', 'bottom')
           for ha in ('left', 'center', 'right')]

xys = [[i,j] for j in np.linspace(0.9,0.1,5) for i in np.linspace(0.1,0.9,5)]
degs = np.linspace(0,360,25)

for ax, align in zip(axes.reshape(-1), aligns):

    ax.set_xlim([-0.1,1.1])
    ax.set_ylim([-0.1,1.1])

    for deg,xy in zip(degs,xys):
        x,y = xy
        ax.plot(x,y,'r.')
        text = ax.text(x,y,'test',
            rotation = deg,
            rotation_mode="anchor",  ### <--- this is the key
            va = align[0],
            ha = align[1],
            bbox=dict(facecolor='none', edgecolor='blue', pad=0.0),
        )
        ax.set_title('alignment = {}'.format(align))

fig.tight_layout()
plt.show()

旧解决方案,子类化 Text

如果仍然有兴趣,solution given by @ThomasKühn当然工作正常,但在非笛卡尔系统中使用文本时有一些缺点,因为它计算数据坐标中所需的偏移量。

以下是代码的一个版本,它通过使用在绘制文本时临时附加的转换来偏移显示坐标中的文本。因此,它也可以用于例如在极坐标图中。

from matplotlib import pyplot as plt
from matplotlib import patches, text
import matplotlib.transforms
import numpy as np

class TextTrueAlign(text.Text):
    """
    A Text object that always aligns relative to the text, not
    to the bounding box; also when the text is rotated.
    """
    def __init__(self, x, y, text, **kwargs):
        super(TextTrueAlign, self).__init__(x,y,text, **kwargs)
        self.__Ha = self.get_ha()
        self.__Va = self.get_va()


    def draw(self, renderer, *args, **kwargs):
        """
        Overload of the Text.draw() function
        """
        trans = self.get_transform()
        offset = self.update_position()
        # while drawing, set a transform which is offset
        self.set_transform(trans + offset)
        super(TextTrueAlign, self).draw(renderer, *args, **kwargs)
        # reset to original transform
        self.set_transform(trans)

    def update_position(self):
        """
        As the (center/center) alignment always aligns to the center of the
        text, even upon rotation, we make use of this here. The algorithm
        first computes the (x,y) offset for the un-rotated text between
        centered alignment and the alignment requested by the user. This offset
        is then rotated by the given rotation angle.
        Finally a translation of the negative offset is returned.
        """
        #resetting to the original state:
        rotation = self.get_rotation()
        self.set_rotation(0)
        self.set_va(self.__Va)
        self.set_ha(self.__Ha)
        ##from https://stackoverflow.com/questions/5320205/matplotlib-text-dimensions
        ##getting the current renderer, so that
        ##get_window_extent() works
        renderer = self.axes.figure.canvas.get_renderer()
        ##computing the bounding box for the un-rotated text
        ##aligned as requested by the user
        bbox1  = self.get_window_extent(renderer=renderer)
        ##re-aligning text to (center,center) as here rotations
        ##do what is intuitively expected
        self.set_va('center')
        self.set_ha('center')
        ##computing the bounding box for the un-rotated text
        ##aligned to (center,center)
        bbox2 = self.get_window_extent(renderer=renderer)
        ##computing the difference vector between the two alignments
        dr = np.array(bbox2.get_points()[0]-bbox1.get_points()[0])
        ##computing the rotation matrix, which also accounts for
        ##the aspect ratio of the figure, to stretch squeeze
        ##dimensions as needed
        rad = np.deg2rad(rotation)
        rot_mat = np.array([
            [np.cos(rad), np.sin(rad)],
            [-np.sin(rad), np.cos(rad)]
        ])
        ##computing the offset vector
        drp = np.dot(dr,rot_mat)        
        # transform to translate by the negative offset vector
        offset = matplotlib.transforms.Affine2D().translate(-drp[0],-drp[1])
        ##setting rotation value back to the one requested by the user
        self.set_rotation(rotation)
        return offset

if __name__ == '__main__':
    fig, axes = plt.subplots(3,3, figsize=(10,10),dpi=100)
    aligns = [ (va,ha) for va in ('top', 'center', 'bottom')
               for ha in ('left', 'center', 'right')]

    xys = [[i,j] for j in np.linspace(0.9,0.1,5) for i in np.linspace(0.1,0.9,5)]
    degs = np.linspace(0,360,25)

    for ax, align in zip(axes.reshape(-1), aligns):

        ax.set_xlim([-0.1,1.1])
        ax.set_ylim([-0.1,1.1])

        for deg,xy in zip(degs,xys):
            x,y = xy
            ax.plot(x,y,'r.')
            text = TextTrueAlign(
                x = x,
                y = y,
                text='test',
                axes = ax,
                rotation = deg,
                va = align[0],
                ha = align[1],
                bbox=dict(facecolor='none', edgecolor='blue', pad=0.0),
            )
            ax.add_artist(text)
            ax.set_title('alignment = {}'.format(align))

    fig.tight_layout()
    plt.show()

关于python - 相对于文本而不是边界框对齐任意旋转的文本注释,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44143395/

相关文章:

Python:如何生成全因子组合?

python - Scrapy-MySQL管道不保存数据

python-3.x - 从列表创建多个绘图

matplotlib - 导入错误: No module named 'matplotlib.pyplot' ; matplotlib is not a package

python - 抛出 ZeroDivisionError

python - 数字列表 - 如何找到重复模式?

python - 查找列表中的最大元素

python - 在pygame python3中创建登录系统

mysql - 如何使数据库可移植

python - 如何向 Seaborn 分布图添加均值线和中线