1. ホーム
  2. python

matplotlib でテキストボックスが行の折り返しを持つ?

2023-09-18 16:38:41

質問

Matplotlibでテキストをボックス内に表示することは可能ですか? 自動改行付き ? そのためには pyplot.text() を使用すると、ウィンドウの境界を越えて流れる複数行のテキストを印刷することしかできず、迷惑しています。 行の大きさは事前に分からないので...何かアイデアがあれば是非お願いします!

どのように解決するのですか?

この解答の内容は https://github.com/matplotlib/matplotlib/pull/4342 でmplマスターにマージされ、次の機能リリースに含まれる予定です。


わあ...。これは茨の道だ...(そして、matplotlibのテキストレンダリングの多くの制限を露呈している...)

これは matplotlib が内蔵しているものであるべきなのですが、そうではありません。 これまでにも、いくつかの スレッドがあります。 しかし、私が見つけた自動テキストラッピングの解決策はありません。

まず最初に、matplotlibで描画する前にレンダリングされたテキスト文字列のサイズ(ピクセル単位)を決定する方法がないのです。 これはあまり大きな問題ではなく、単に描画してサイズを取得し、そしてラップされたテキストを再描画すればよいのです。 (それは高価ですが、あまり過度に悪いわけではありません)

次の問題は、文字がピクセル単位で固定された幅を持っていないことです。したがって、特定の文字数にテキスト文字列をラップしても、レンダリング時に必ずしも特定の幅が反映されるわけではありません。 これは大きな問題ではありませんが。

それ以上に、これを一度だけ行うことはできません...。さもなければ、最初に (たとえば画面上で) 描画したときは正しく折り返されますが、もう一度 (図をリサイズしたり、画面と異なる DPI の画像として保存したりしたとき) 描画したときは正しく折り返されません。 これは大きな問題ではなく、コールバック関数を matplotlib の draw イベントに接続すれば良いのです。

とにかく、この解決策は不完全ですが、ほとんどの状況でうまくいくはずです。私は、テキストレンダリングされた文字列、引き伸ばされたフォント、または異常なアスペクト比を持つフォントを考慮しようとしません。しかし、回転したテキストを適切に処理できるようになりました。

しかし、複数のサブプロットにあるテキストオブジェクトを、あなたがを接続するどの図形でも自動的にラップしようとするはずです。 on_draw コールバックを接続する... 多くの場合、それは不完全でしょうが、ちゃんとした仕事をします。

import matplotlib.pyplot as plt

def main():
    fig = plt.figure()
    plt.axis([0, 10, 0, 10])

    t = "This is a really long string that I'd rather have wrapped so that it"\
    " doesn't go outside of the figure, but if it's long enough it will go"\
    " off the top or bottom!"
    plt.text(4, 1, t, ha='left', rotation=15)
    plt.text(5, 3.5, t, ha='right', rotation=-15)
    plt.text(5, 10, t, fontsize=18, ha='center', va='top')
    plt.text(3, 0, t, family='serif', style='italic', ha='right')
    plt.title("This is a really long title that I want to have wrapped so it"\
             " does not go outside the figure boundaries", ha='center')

    # Now make the text auto-wrap...
    fig.canvas.mpl_connect('draw_event', on_draw)
    plt.show()

def on_draw(event):
    """Auto-wraps all text objects in a figure at draw-time"""
    import matplotlib as mpl
    fig = event.canvas.figure

    # Cycle through all artists in all the axes in the figure
    for ax in fig.axes:
        for artist in ax.get_children():
            # If it's a text artist, wrap it...
            if isinstance(artist, mpl.text.Text):
                autowrap_text(artist, event.renderer)

    # Temporarily disconnect any callbacks to the draw event...
    # (To avoid recursion)
    func_handles = fig.canvas.callbacks.callbacks[event.name]
    fig.canvas.callbacks.callbacks[event.name] = {}
    # Re-draw the figure..
    fig.canvas.draw()
    # Reset the draw event callbacks
    fig.canvas.callbacks.callbacks[event.name] = func_handles

def autowrap_text(textobj, renderer):
    """Wraps the given matplotlib text object so that it exceed the boundaries
    of the axis it is plotted in."""
    import textwrap
    # Get the starting position of the text in pixels...
    x0, y0 = textobj.get_transform().transform(textobj.get_position())
    # Get the extents of the current axis in pixels...
    clip = textobj.get_axes().get_window_extent()
    # Set the text to rotate about the left edge (doesn't make sense otherwise)
    textobj.set_rotation_mode('anchor')

    # Get the amount of space in the direction of rotation to the left and 
    # right of x0, y0 (left and right are relative to the rotation, as well)
    rotation = textobj.get_rotation()
    right_space = min_dist_inside((x0, y0), rotation, clip)
    left_space = min_dist_inside((x0, y0), rotation - 180, clip)

    # Use either the left or right distance depending on the horiz alignment.
    alignment = textobj.get_horizontalalignment()
    if alignment is 'left':
        new_width = right_space 
    elif alignment is 'right':
        new_width = left_space
    else:
        new_width = 2 * min(left_space, right_space)

    # Estimate the width of the new size in characters...
    aspect_ratio = 0.5 # This varies with the font!! 
    fontsize = textobj.get_size()
    pixels_per_char = aspect_ratio * renderer.points_to_pixels(fontsize)

    # If wrap_width is < 1, just make it 1 character
    wrap_width = max(1, new_width // pixels_per_char)
    try:
        wrapped_text = textwrap.fill(textobj.get_text(), wrap_width)
    except TypeError:
        # This appears to be a single word
        wrapped_text = textobj.get_text()
    textobj.set_text(wrapped_text)

def min_dist_inside(point, rotation, box):
    """Gets the space in a given direction from "point" to the boundaries of
    "box" (where box is an object with x0, y0, x1, & y1 attributes, point is a
    tuple of x,y, and rotation is the angle in degrees)"""
    from math import sin, cos, radians
    x0, y0 = point
    rotation = radians(rotation)
    distances = []
    threshold = 0.0001 
    if cos(rotation) > threshold: 
        # Intersects the right axis
        distances.append((box.x1 - x0) / cos(rotation))
    if cos(rotation) < -threshold: 
        # Intersects the left axis
        distances.append((box.x0 - x0) / cos(rotation))
    if sin(rotation) > threshold: 
        # Intersects the top axis
        distances.append((box.y1 - y0) / sin(rotation))
    if sin(rotation) < -threshold: 
        # Intersects the bottom axis
        distances.append((box.y0 - y0) / sin(rotation))
    return min(distances)

if __name__ == '__main__':
    main()

<イグ