Текстовое поле с переносом строк в matplotlib?
есть ли возможность отображения текста в окне с помощью библиотек matplotlib, с автоматической разрывы строк? С помощью pyplot.text()
, мне удалось напечатать только многострочный текст, который выходит за пределы окна, что раздражает. Размер линий заранее не известен... любая идея будет высоко оценена!
3 ответа:
содержимое этого ответа было объединено в MPL master в https://github.com/matplotlib/matplotlib/pull/4342 и будет в следующем выпуске функции.
Вау... Это очень сложная проблема... (И он предоставляет много ограничений в рендеринге текста matplotlib...)
Это должно быть (i.m.o.) что-то, что matplotlib имеет встроенный, но это не так. было несколько темы про это в списке рассылки, но нет решение, которое я мог бы найти для автоматической упаковки текста.
Итак, во-первых, нет способа определить размер (в пикселях) отрисованной текстовой строки до ее рисования в matplotlib. Это не слишком большая проблема, так как мы можем просто нарисовать ее, получить размер, а затем перерисовать обернутый текст. (Это дорого, но не слишком плохо)
следующая проблема заключается в том, что символы не имеют фиксированной ширины в пикселях, поэтому перенос текстовой строки на заданное количество символов не обязательно будет отражать заданную ширину при визуализации. Но это не такая уж большая проблема.
помимо этого, мы не можем просто сделать это один раз... В противном случае он будет правильно обернут при первом рисовании (например, на экране), но не при повторном рисовании (когда рисунок изменяется или сохраняется как изображение с другим DPI, чем экран). Это не огромная проблема, так как мы можем просто подключить функцию обратного вызова к событию рисования matplotlib.
во всяком случае это решение несовершенно, но оно должно работать в большинстве ситуаций. Я не пытаюсь учитывать строки, отображаемые tex, любые растянутые шрифты или шрифты с необычным соотношением сторон. Однако теперь он должен правильно обрабатывать повернутый текст.
однако он должен попытаться автоматически обернуть любые текстовые объекты в несколько подзаголовков в зависимости от того, какие цифры вы соединяете
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()
прошло примерно пять лет, но до сих пор не кажется, что это отличный способ сделать это. Вот моя версия принятого решения. Моя цель состояла в том, чтобы позволить выборочно применять пиксельную обертку к отдельным экземплярам текста. Я также создал простую функцию textBox (), которая преобразует любые оси в текстовое поле с пользовательскими полями и выравниванием.
вместо того, чтобы предполагать определенное соотношение сторон шрифта или среднюю ширину, я на самом деле рисую строку одним словом в a время и вставка новых строк после достижения порога. Это ужасно медленно по сравнению с приближениями, но все еще чувствует себя довольно быстро для строк
# 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 или сохранение исходной строки где-то и "сброс" экземпляра текста между обертками.