簡體   English   中英

如何旋轉 matplotlib 注釋以匹配一條線?

[英]How to rotate matplotlib annotation to match a line?

有一個 plot 有幾條斜率不同的對角線。 我想用與線條斜率匹配的文本 label 來注釋這些線條。

像這樣:

注釋行

有沒有可靠的方法來做到這一點?

我已經嘗試了textannotate的旋轉參數,但它們是在屏幕坐標中,而不是數據坐標(即無論 xy 范圍如何,它在屏幕上總是x度)。 我的xy范圍相差幾個數量級,明顯的斜率受視口大小和其他變量的影響,因此固定度數的旋轉不起作用。 還有其他想法嗎?

我想出了一些對我有用的東西。 注意灰色虛線:

注釋行

旋轉必須手動設置,但這必須在draw()或布局之后完成。 所以我的解決方案是將行與注釋相關聯,然后遍歷它們並執行以下操作:

  1. 獲取行的數據變換(即從數據坐標到顯示坐標)
  2. 沿線變換兩點以顯示坐標
  3. 找到顯示線的斜率
  4. 設置文本旋轉以匹配此斜率

這並不完美,因為 matplotlib 對旋轉文本的處理完全是錯誤的。 它按邊界框對齊,而不是按文本基線對齊。

如果您對文本渲染感興趣,可以了解一些字體基礎知識: http : //docs.oracle.com/javase/tutorial/2d/text/fontconcepts.html

這個例子展示了 matplotlib 的作用: http : //matplotlib.org/examples/pylab_examples/text_rotation.html

我發現在線條旁邊正確放置標簽的唯一方法是在垂直和水平方向按中心對齊。 然后我將標簽向左偏移 10 個點以使其不重疊。 對我的申請來說已經足夠了。

這是我的代碼。 我隨意繪制線條,然后繪制注釋,然后將它們與輔助函數綁定:

line, = fig.plot(xdata, ydata, '--', color=color)

# x,y appear on the midpoint of the line

t = fig.annotate("text", xy=(x, y), xytext=(-10, 0), textcoords='offset points', horizontalalignment='left', verticalalignment='bottom', color=color)
text_slope_match_line(t, x, y, line)

然后在布局之后但在savefig之前調用另一個輔助函數(對於交互式圖像,我認為您必須注冊繪制事件並在處理程序中調用update_text_slopes

plt.tight_layout()
update_text_slopes()

幫手們:

rotated_labels = []
def text_slope_match_line(text, x, y, line):
    global rotated_labels

    # find the slope
    xdata, ydata = line.get_data()

    x1 = xdata[0]
    x2 = xdata[-1]
    y1 = ydata[0]
    y2 = ydata[-1]

    rotated_labels.append({"text":text, "line":line, "p1":numpy.array((x1, y1)), "p2":numpy.array((x2, y2))})

def update_text_slopes():
    global rotated_labels

    for label in rotated_labels:
        # slope_degrees is in data coordinates, the text() and annotate() functions need it in screen coordinates
        text, line = label["text"], label["line"]
        p1, p2 = label["p1"], label["p2"]

        # get the line's data transform
        ax = line.get_axes()

        sp1 = ax.transData.transform_point(p1)
        sp2 = ax.transData.transform_point(p2)

        rise = (sp2[1] - sp1[1])
        run = (sp2[0] - sp1[0])

        slope_degrees = math.degrees(math.atan(rise/run))

        text.set_rotation(slope_degrees)

這與@Adam 給出的過程和基本代碼完全相同——它只是被重組為(希望)更方便一些。

def label_line(line, label, x, y, color='0.5', size=12):
    """Add a label to a line, at the proper angle.

    Arguments
    ---------
    line : matplotlib.lines.Line2D object,
    label : str
    x : float
        x-position to place center of text (in data coordinated
    y : float
        y-position to place center of text (in data coordinates)
    color : str
    size : float
    """
    xdata, ydata = line.get_data()
    x1 = xdata[0]
    x2 = xdata[-1]
    y1 = ydata[0]
    y2 = ydata[-1]

    ax = line.get_axes()
    text = ax.annotate(label, xy=(x, y), xytext=(-10, 0),
                       textcoords='offset points',
                       size=size, color=color,
                       horizontalalignment='left',
                       verticalalignment='bottom')

    sp1 = ax.transData.transform_point((x1, y1))
    sp2 = ax.transData.transform_point((x2, y2))

    rise = (sp2[1] - sp1[1])
    run = (sp2[0] - sp1[0])

    slope_degrees = np.degrees(np.arctan2(rise, run))
    text.set_rotation(slope_degrees)
    return text

像這樣使用:

import numpy as np
import matplotlib.pyplot as plt

...
fig, axes = plt.subplots()
color = 'blue'
line, = axes.plot(xdata, ydata, '--', color=color)
...
label_line(line, "Some Label", x, y, color=color)

編輯:注意這個方法還需要在圖形布局完成調用,否則會發生變化。

參見: https : //gist.github.com/lzkelley/0de9e8bf2a4fe96d2018f1b1bd5a0d3c

matplotlib 3.4.0 新增功能

現在有一個內置參數transform_rotates_text用於相對於一行旋轉文本:

要相對於一條線旋轉文本,正確的角度不是該線在 plot 坐標系中的角度,而是該線出現在屏幕坐標系中的角度。 這個角度可以通過設置新參數transform_rotates_text自動確定。

所以現在我們可以將原始數據角度傳遞給plt.text並讓 matplotlib 通過設置transform_rotates_text=True自動將其轉換為正確的視角:

# plot line from (1, 4) to (6, 10)
x = [1, 6]
y = [4, 10]
plt.plot(x, y, 'r.-')

# compute angle in raw data coordinates (no manual transforms)
dy = y[1] - y[0]
dx = x[1] - x[0]
angle = np.rad2deg(np.arctan2(dy, dx))

# annotate with transform_rotates_text to align text and line
plt.text(x[0], y[0], f'rotation={angle:.2f}', ha='left', va='bottom',
         transform_rotates_text=True, rotation=angle, rotation_mode='anchor')

這種方法對圖形和軸比例具有魯棒性。 即使我們在放置文本修改figsizexlim ,旋轉也會保持正確對齊:

# resizing the figure won't mess up the rotation
plt.gcf().set_size_inches(9, 4)

# rescaling the axes won't mess up the rotation
plt.xlim(0, 12)

盡管這個問題很老,但我不斷遇到它並感到沮喪,因為它不太有效。 我將它改造成一個類LineAnnotation和 helper line_annotate這樣它

  1. 使用特定點x處的斜率,
  2. 適用於重新布局和調整大小,以及
  3. 接受垂直於斜率的相對偏移。
x = np.linspace(np.pi, 2*np.pi)
line, = plt.plot(x, np.sin(x))

for x in [3.5, 4.0, 4.5, 5.0, 5.5, 6.0]:
    line_annotate(str(x), line, x)

注釋竇

我最初將其放入 公共要點中,但 @Adam 要求我將其包含在此處。

import numpy as np
from matplotlib.text import Annotation
from matplotlib.transforms import Affine2D


class LineAnnotation(Annotation):
    """A sloped annotation to *line* at position *x* with *text*
    Optionally an arrow pointing from the text to the graph at *x* can be drawn.
    Usage
    -----
    fig, ax = subplots()
    x = linspace(0, 2*pi)
    line, = ax.plot(x, sin(x))
    ax.add_artist(LineAnnotation("text", line, 1.5))
    """

    def __init__(
        self, text, line, x, xytext=(0, 5), textcoords="offset points", **kwargs
    ):
        """Annotate the point at *x* of the graph *line* with text *text*.

        By default, the text is displayed with the same rotation as the slope of the
        graph at a relative position *xytext* above it (perpendicularly above).

        An arrow pointing from the text to the annotated point *xy* can
        be added by defining *arrowprops*.

        Parameters
        ----------
        text : str
            The text of the annotation.
        line : Line2D
            Matplotlib line object to annotate
        x : float
            The point *x* to annotate. y is calculated from the points on the line.
        xytext : (float, float), default: (0, 5)
            The position *(x, y)* relative to the point *x* on the *line* to place the
            text at. The coordinate system is determined by *textcoords*.
        **kwargs
            Additional keyword arguments are passed on to `Annotation`.

        See also
        --------
        `Annotation`
        `line_annotate`
        """
        assert textcoords.startswith(
            "offset "
        ), "*textcoords* must be 'offset points' or 'offset pixels'"

        self.line = line
        self.xytext = xytext

        # Determine points of line immediately to the left and right of x
        xs, ys = line.get_data()

        def neighbours(x, xs, ys, try_invert=True):
            inds, = np.where((xs <= x)[:-1] & (xs > x)[1:])
            if len(inds) == 0:
                assert try_invert, "line must cross x"
                return neighbours(x, xs[::-1], ys[::-1], try_invert=False)

            i = inds[0]
            return np.asarray([(xs[i], ys[i]), (xs[i+1], ys[i+1])])
        
        self.neighbours = n1, n2 = neighbours(x, xs, ys)
        
        # Calculate y by interpolating neighbouring points
        y = n1[1] + ((x - n1[0]) * (n2[1] - n1[1]) / (n2[0] - n1[0]))

        kwargs = {
            "horizontalalignment": "center",
            "rotation_mode": "anchor",
            **kwargs,
        }
        super().__init__(text, (x, y), xytext=xytext, textcoords=textcoords, **kwargs)

    def get_rotation(self):
        """Determines angle of the slope of the neighbours in display coordinate system
        """
        transData = self.line.get_transform()
        dx, dy = np.diff(transData.transform(self.neighbours), axis=0).squeeze()
        return np.rad2deg(np.arctan2(dy, dx))

    def update_positions(self, renderer):
        """Updates relative position of annotation text
        Note
        ----
        Called during annotation `draw` call
        """
        xytext = Affine2D().rotate_deg(self.get_rotation()).transform(self.xytext)
        self.set_position(xytext)
        super().update_positions(renderer)


def line_annotate(text, line, x, *args, **kwargs):
    """Add a sloped annotation to *line* at position *x* with *text*

    Optionally an arrow pointing from the text to the graph at *x* can be drawn.

    Usage
    -----
    x = linspace(0, 2*pi)
    line, = ax.plot(x, sin(x))
    line_annotate("sin(x)", line, 1.5)

    See also
    --------
    `LineAnnotation`
    `plt.annotate`
    """
    ax = line.axes
    a = LineAnnotation(text, line, x, *args, **kwargs)
    if "clip_on" in kwargs:
        a.set_clip_path(ax.patch)
    ax.add_artist(a)
    return a

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM