Можно ли отображать текст в поле через Matplotlib с автоматическими разрывами строк? Используя pyplot.text()
, я смог распечатать многострочный текст, который выходит за границы окна, что раздражает. Размер линий неизвестен заранее... Любая идея была бы высоко оценена!
Текстовое поле с переводом строки в matplotlib?
Ответ 1
Содержимое этого ответа было объединено в mpl master в https://github.com/matplotlib/matplotlib/pull/4342 и будет в следующем выпуске функции.
Вау... Это сложная проблема... (И она предоставляет множество ограничений в текстовом рендеринге matplotlib...)
Это должно (i.m.o.) быть тем, что matplotlib имеет встроенный, но это не так. В списке рассылки было несколько , но не было решения, которое можно было бы найти для автоматической переносимости текста.
Итак, во-первых, нет способа определить размер (в пикселях) отображаемой текстовой строки перед тем, как он нарисован в matplotlib. Это не слишком большая проблема, так как мы можем просто нарисовать ее, получить размер, а затем перерисовать завернутый текст. (Это дорого, но не слишком сильно)
Следующая проблема заключается в том, что символы не имеют фиксированной ширины в пикселях, поэтому перенос текстовой строки на заданное количество символов не обязательно будет отражать заданную ширину при визуализации. Однако это не большая проблема.
Кроме того, мы не можем просто сделать это один раз... В противном случае он будет правильно упакован, когда будет нарисован в первый раз (например, на экране), но не в случае повторного рисования (при изменении размера или сохранен как изображение с другим DPI, чем экран). Это не является большой проблемой, так как мы можем просто подключить функцию обратного вызова к событию draw 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 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 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()
Ответ 2
Его было примерно пять лет, но, похоже, это отличный способ сделать это. Вот моя версия принятого решения. Моя цель состояла в том, чтобы позволить выборочную упаковку с пиксельным эффектом выборочно применяться к отдельным экземплярам текста. Я также создал простую функцию 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() вставляет в строку дополнительные символы новой строки, но не имеет возможности их удалить. Это может быть разрешено путем удаления всех \n символов в функции _wrap или сохранения исходной строки где-нибудь и "сброса" экземпляра текста между обертками.