matplotlib에서 줄 바꿈이있는 텍스트 상자?
자동 줄 바꿈을 사용 하여 Matplotlib를 통해 상자에 텍스트를 표시 할 수 있습니까? 를 사용 pyplot.text()하여 창 경계를 넘어가는 여러 줄 텍스트 만 인쇄 할 수 있었는데, 이는 성가신 일이었습니다. 선의 크기는 미리 알려지지 않았습니다. 어떤 아이디어라도 감사하겠습니다!
이 답변의 내용은 https://github.com/matplotlib/matplotlib/pull/4342의 mpl master에 병합되었으며 다음 기능 릴리스에 포함될 것입니다.
와우 ... 이것은 까다로운 문제입니다 ... (그리고 matplotlib의 텍스트 렌더링에 많은 제한이 있습니다 ...)
이것은 (imo) matplotlib에 내장 된 것이어야하지만 그렇지 않습니다. 메일 링리스트에 몇 개의 스레드가 있지만 자동 텍스트 줄 바꿈에 대한 해결책이 없습니다.
따라서 먼저 렌더링 된 텍스트 문자열이 matplotlib에 그려지기 전에 크기 (픽셀 단위)를 결정할 방법이 없습니다. 그리 큰 문제는 아닙니다. 그림을 그리고 크기를 얻은 다음 줄 바꿈 된 텍스트를 다시 그릴 수 있기 때문입니다. (비싸지 만 너무 나쁘지는 않습니다)
다음 문제는 문자의 너비가 픽셀 단위로 고정되어 있지 않기 때문에 텍스트 문자열을 주어진 문자 수로 래핑한다고해서 렌더링 될 때 반드시 주어진 너비가 반영되지는 않습니다. 하지만 이것은 큰 문제는 아닙니다.
그 외에는 한 번만 할 수 없습니다. 그렇지 않으면 처음으로 그릴 때 (예 : 화면에) 올바르게 래핑되지만 다시 그릴 때는 (그림 크기가 조정되거나 화면과 DPI가 다른 이미지). 콜백 함수를 matplotlib 그리기 이벤트에 연결할 수 있기 때문에 이것은 큰 문제가 아닙니다.
어쨌든이 솔루션은 불완전하지만 대부분의 상황에서 작동합니다. tex-rendered 문자열, 늘어난 글꼴 또는 비정상적인 종횡비를 가진 글꼴을 고려하지 않습니다. 그러나 이제 회전 된 텍스트를 제대로 처리해야합니다.
그러나 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()

wrap = True아래 예제와 같이 텍스트 상자를 만들 때 설정합니다 . 이것은 원하는 효과를 가질 수 있습니다.
plt.text(5, 5, t, ha='right', rotation=-15, wrap=True)
대략 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 문자를 모두 제거하거나 원래 문자열을 어딘가에 저장하고 줄 바꿈 사이에 텍스트 인스턴스를 "재설정"하여 해결할 수 있습니다.
참고 URL : https://stackoverflow.com/questions/4018860/text-box-with-line-wrapping-in-matplotlib
'Program Club' 카테고리의 다른 글
| SQL Server와 같은 절에서 조인을 사용하여 select 절에서 Postgresql 하위 쿼리를 수행하는 방법은 무엇입니까? (0) | 2020.11.04 |
|---|---|
| 여러 프로젝트 및 구성에 Visual Studio 프로젝트 속성을 효과적으로 사용 (0) | 2020.11.04 |
| 환경 설정이 변경되었는지 감지하는 방법은 무엇입니까? (0) | 2020.11.04 |
| THREE.js에서 텍스처 사용 (0) | 2020.11.04 |
| RestTemplate을 사용할 때 내부 Jackson 매퍼를 어떻게 구성 할 수 있습니까? (0) | 2020.11.04 |
