[Python] matplotlib에 줄 바꿈이 포함 된 텍스트 상자?


Answers

이 답변의 내용은 https://github.com/matplotlib/matplotlib/pull/4342 에있는 mpl master에 병합되었으며 다음 기능 릴리스에 포함될 것입니다.

와우 ... 이것은 어려운 문제입니다 ... (그리고 그것은 matplotlib의 텍스트 렌더링에서 많은 한계를 드러냅니다 ...)

이것은 (imo) matplotlib에 내장되어있는 것이어야하지만 그렇지 않습니다. 메일 링리스트에는 몇 줄의 스레드가 있지만 자동 텍스트 배치에 대한 해결책은 없습니다.

먼저, 렌더링 된 텍스트 문자열이 matplotlib에 그려지기 전에 크기 (픽셀 단위)를 결정할 방법이 없습니다. 크기를 얻은 다음 줄 바꿈 된 텍스트를 다시 그릴 수 있기 때문에 너무 큰 문제는 아닙니다. (비싸지 만 지나치게 나쁘지는 않습니다)

다음 문제는 문자의 너비가 픽셀 단위로 고정되어 있지 않기 때문에 텍스트 문자열을 주어진 문자 수로 래핑 할 때 반드시 렌더링 할 때 주어진 너비를 반영하지는 않는다는 것입니다. 하지만 이것은 큰 문제는 아닙니다.

그 이상으로, 우리는 이것을 한 번만 할 수는 없습니다 ... 그렇지 않으면 처음으로 그려지는 경우 (예를 들어 화면에서) 올바르게 래핑되지만 다시 그려지는 경우 (그림이 크기가 조정되거나 이미지가 화면과 다른 DPI로 표시됨). 커다란 문제는 아니며, 콜백 함수를 matplotlib 그리기 이벤트에 연결할 수 있습니다.

어쨌든이 솔루션은 불완전하지만 대부분의 상황에서 작동해야합니다. 나는 텍 스트 렌더링 문자열, 확장 된 폰트 또는 비정상적인 종횡비를 가진 폰트를 고려하지 않는다. 그러나 이제 회전 된 텍스트를 올바르게 처리해야합니다.

그러나 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()

Question

Matplotlib을 통해 자동 줄 바꿈으로 텍스트를 상자에 표시 할 수 있습니까? pyplot.text() 를 사용하여 창 테두리를 넘는 여러 줄의 텍스트 만 인쇄 할 수있었습니다. 이는 성가신 일입니다. 선의 크기는 미리 알려지지 않았습니다 ... 어떤 아이디어라도 높이 평가할 것입니다!




그것의 대략 5 년 그러나 아직도 이것을하는 중대한 방법 인 것처럼 보이지 않는다. 여기에 허용 된 솔루션의 제 버전이 있습니다. 내 목표는 개별 텍스트 인스턴스에 선택적으로 픽셀 완전 포장을 적용하는 것이 었습니다. 또한 모든 축을 사용자 정의 여백과 정렬이있는 텍스트 상자로 변환하는 간단한 textBox () 함수를 만들었습니다.

특정 글꼴 종횡비 또는 평균 너비를 가정하는 대신 실제로 한 번에 한 단어 씩 그어보고 임계 값에 도달하면 줄 바꿈을 삽입합니다. 이것은 근사값에 비해 끔찍하게 느리지 만 문자열이 200 단어 미만인 경우에도 매우 느껴집니다.

# Text Wrapping
# Defines wrapText which will attach an event to a given mpl.text object,
# wrapping it within the parent axes object.  Also defines a the convenience
# function textBox() which effectively converts an axes to a text box.
def wrapText(text, margin=4):
    """ Attaches an on-draw event to a given mpl.text object which will
        automatically wrap its string wthin the parent axes object.

        The margin argument controls the gap between the text and axes frame
        in points.
    """
    ax = text.get_axes()
    margin = margin / 72 * ax.figure.get_dpi()

    def _wrap(event):
        """Wraps text within its parent axes."""
        def _width(s):
            """Gets the length of a string in pixels."""
            text.set_text(s)
            return text.get_window_extent().width

        # Find available space
        clip = ax.get_window_extent()
        x0, y0 = text.get_transform().transform(text.get_position())
        if text.get_horizontalalignment() == 'left':
            width = clip.x1 - x0 - margin
        elif text.get_horizontalalignment() == 'right':
            width = x0 - clip.x0 - margin
        else:
            width = (min(clip.x1 - x0, x0 - clip.x0) - margin) * 2

        # Wrap the text string
        words = [''] + _splitText(text.get_text())[::-1]
        wrapped = []

        line = words.pop()
        while words:
            line = line if line else words.pop()
            lastLine = line

            while _width(line) <= width:
                if words:
                    lastLine = line
                    line += words.pop()
                    # Add in any whitespace since it will not affect redraw width
                    while words and (words[-1].strip() == ''):
                        line += words.pop()
                else:
                    lastLine = line
                    break

            wrapped.append(lastLine)
            line = line[len(lastLine):]
            if not words and line:
                wrapped.append(line)

        text.set_text('\n'.join(wrapped))

        # Draw wrapped string after disabling events to prevent recursion
        handles = ax.figure.canvas.callbacks.callbacks[event.name]
        ax.figure.canvas.callbacks.callbacks[event.name] = {}
        ax.figure.canvas.draw()
        ax.figure.canvas.callbacks.callbacks[event.name] = handles

    ax.figure.canvas.mpl_connect('draw_event', _wrap)

def _splitText(text):
    """ Splits a string into its underlying chucks for wordwrapping.  This
        mostly relies on the textwrap library but has some additional logic to
        avoid splitting latex/mathtext segments.
    """
    import textwrap
    import re
    math_re = re.compile(r'(?<!\\)\$')
    textWrapper = textwrap.TextWrapper()

    if len(math_re.findall(text)) <= 1:
        return textWrapper._split(text)
    else:
        chunks = []
        for n, segment in enumerate(math_re.split(text)):
            if segment and (n % 2):
                # Mathtext
                chunks.append('${}$'.format(segment))
            else:
                chunks += textWrapper._split(segment)
        return chunks

def textBox(text, axes, ha='left', fontsize=12, margin=None, frame=True, **kwargs):
    """ Converts an axes to a text box by removing its ticks and creating a
        wrapped annotation.
    """
    if margin is None:
        margin = 6 if frame else 0
    axes.set_xticks([])
    axes.set_yticks([])
    axes.set_frame_on(frame)

    an = axes.annotate(text, fontsize=fontsize, xy=({'left':0, 'right':1, 'center':0.5}[ha], 1), ha=ha, va='top',
                       xytext=(margin, -margin), xycoords='axes fraction', textcoords='offset points', **kwargs)
    wrapText(an, margin=margin)
    return an

용법:

ax = plot.plt.figure(figsize=(6, 6)).add_subplot(111)
an = ax.annotate(t, fontsize=12, xy=(0.5, 1), ha='center', va='top', xytext=(0, -6),
                 xycoords='axes fraction', textcoords='offset points')
wrapText(an)

나에게별로 중요하지 않은 몇 가지 기능을 떨어 뜨 렸습니다. _wrap ()을 호출 할 때마다 줄 바꿈을 추가하지만 제거 할 방법이 없으므로 크기 조정이 실패합니다. 이것은 _wrap 함수의 모든 \ n 문자를 제거하거나 원래 문자열을 어딘가에 저장하고 랩간에 텍스트 인스턴스를 "재설정"하여 해결할 수 있습니다.