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

Tho*_*ühn 2 python matplotlib python-3.x

在尝试回答一个旧的未解决的问题时,我遇到了一个有关文本批注的小问题matplotlib:在将旋转的文本添加到图形的某个位置时,文本相对于文本的边界框对齐,而不是相对于(虚构的)对齐包含文本本身的旋转框。最好用一个小例子来解释一下: matplotlib中文本对齐的示例

该图显示了具有不同旋转角度和不同对齐选项的文本。对于每个文本对象,红点表示赋予该ax.text()功能的坐标。蓝色框是围绕文本的旋转框架,黑色框是文本的近似边界框(有点太大,但是应该可以理解)。很容易看出,对于对齐位于边缘(左,右,顶部,底部)的情况,红点位于边界框的侧面或边缘,而不是文本框。唯一以直观方式对齐文本的对齐方式是将水平和垂直对齐方式都设置为“居中”。现在,这不是错误,而是此处概述的预期行为。但是,在某些情况下,它不是很实用,因为必须“手动”调整位置以使文本位于所需位置,并且如果旋转角度发生更改或图形重新缩放,此调整也会更改。

问题是,是否存在一种可靠的方式来生成与文本框架而不是边界框对齐的文本。我已经有解决该问题的方法,但是要弄清楚这很繁琐,所以我想与您分享。

Tho*_*ühn 5

在对matplotlib代码本身进行了一些搜索和挖掘之后,并从这里这里得到了一些启发,我提出了以下解决方案:

from matplotlib import pyplot as plt
from matplotlib import patches, text
import numpy as np
import math


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().__init__(x,y,text, **kwargs)
        self.__Ha = self.get_ha()
        self.__Va = self.get_va()
        self.__Rotation = self.get_rotation()
        self.__Position = self.get_position()

    def draw(self, renderer, *args, **kwargs):
        """
        Overload of the Text.draw() function
        """
        self.update_position()
        super().draw(renderer, *args, **kwargs)

    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 transformed according to the requested rotation angle and the
        aspect ratio of the graph. Finally the transformed offset is used to
        shift the text such that the alignment point coincides with the
        requested coordinate also when the text is rotated.
        """

        #resetting to the original state:
        self.set_rotation(0)
        self.set_va(self.__Va)
        self.set_ha(self.__Ha)
        self.set_position(self.__Position)

        ax = self.axes
        xy = self.__Position

        ##determining the aspect ratio:
        ##from /sf/ask/2911802421/
        ##data limits
        xlim = ax.get_xlim()
        ylim = ax.get_ylim()
        ## Axis size on figure
        figW, figH = ax.get_figure().get_size_inches()
        ## Ratio of display units
        _, _, w, h = ax.get_position().bounds
        ##final aspect ratio
        aspect = ((figW * w)/(figH * h))*(ylim[1]-ylim[0])/(xlim[1]-xlim[0])


        ##from /sf/ask/372414381/
        ##getting the current renderer, so that
        ##get_window_extent() works
        renderer = ax.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)
        bbox1d = ax.transData.inverted().transform(bbox1)

        width  = bbox1d[1,0]-bbox1d[0,0]
        height = bbox1d[1,1]-bbox1d[0,1]

        ##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)
        bbox2d = ax.transData.inverted().transform(bbox2)

        ##computing the difference vector between the two
        ##alignments
        dr = np.array(bbox2d[0]-bbox1d[0])

        ##computing the rotation matrix, which also accounts for
        ##the aspect ratio of the figure, to stretch squeeze
        ##dimensions as needed
        rad = np.deg2rad(self.__Rotation)
        rot_mat = np.array([
            [math.cos(rad), math.sin(rad)*aspect],
            [-math.sin(rad)/aspect, math.cos(rad)]
        ])

        ##computing the offset vector
        drp = np.dot(dr,rot_mat)

        ##setting new position
        self.set_position((xy[0]-drp[0],xy[1]-drp[1]))

        ##setting rotation value back to the one requested by the user
        self.set_rotation(self.__Rotation)




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):
            ax.plot(*xy,'r.')
            text = TextTrueAlign(
                x = xy[0],
                y = xy[1],
                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()
Run Code Online (Sandbox Code Playgroud)

该示例有些冗长,因为我必须编写一个派生自matplotlib.text.Text该类的类,以便在重绘时正确更新文本对象(例如,如果图形被重新缩放)。如果水平和垂直对齐方式都设置为“中心”,则该代码依赖于始终与文本中心对齐的文本。它使用具有中心对齐方式和请求的对齐方式的文本的边界框之间的差值来预测偏移量,旋转后文本需要偏移此偏移量。该示例的输出如下所示: 显示结果的示例TextTrueAlign 为一体的纵横比graphaxes以及figure考虑到,这种做法也是鲁棒于该图的尺寸调整。

我认为,通过治疗方法set_ha()set_va()set_rotation()set_position()我的方式,我可能会打破一些原有的功能matplotlib.text.Text,但应该通过重载这些函数和更换一些比较容易解决selfsuper()

任何意见或建议如何改善这一点将不胜感激。此外,如果您碰巧要对此进行测试并发现任何错误或缺陷,请告诉我,我们将尝试对其进行修复。希望这对某人有用:)


Imp*_*est 5

新解决方案 rotation_mode="anchor"

其实有一种说法rotation_modematplotlib.text.Text,这正是操纵所请求的功能。默认是rotation_mode="default"从问题中重新创建不需要的行为,同时rotation_mode="anchor"根据文本本身而不是其边界框来锚定旋转点。

ax.text(x,y,'test', rotation = deg, rotation_mode="anchor")
Run Code Online (Sandbox Code Playgroud)

另请参阅demo_text_rotation_mode 示例

有了这个,可以轻松创建问题中的示例,而无需将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()
Run Code Online (Sandbox Code Playgroud)

旧解决方案,子类化 Text

如果仍然感兴趣,@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 /sf/ask/372414381/
        ##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()
Run Code Online (Sandbox Code Playgroud)