Text field with line feed in matplotlib?

Is it possible to display text in a field through Matplotlib with automatic line breaks? Using pyplot.text() , I was able to print multiline text that goes beyond the borders of the window, which is annoying. The line size is not known in advance ... Any idea would be much appreciated!

+58
python matplotlib textbox
Oct 25 2018-10-10T00:
source share
2 answers

The content of this answer has been merged into mpl master at https://github.com/matplotlib/matplotlib/pull/4342 and will be in the next release of the function.




Wow ... This is a difficult problem ... (And it provides many limitations in matplotlib text rendering ...)

This should (imo) be what matplotlib has inline, but it is not. There were several on the mailing list, but there was no solution that could be found for automatic text portability.

So, firstly, there is no way to determine the size (in pixels) of the displayed text string before it is drawn in matplotlib. This is not a big problem, since we can just draw it, get the size, and then redraw the wrapped text. (It's expensive, but not too much)

The next problem is that the characters do not have a fixed width in pixels, so wrapping a text string by a given number of characters will not necessarily reflect the specified width when rendering. However, this is not a big problem.

In addition, we cannot just do it once ... Otherwise, it will be correctly packed when it is drawn for the first time (for example, on the screen), but not in the case of repeated drawing (when resizing or saved as an image with other DPI than the screen). This is not a big problem, as we can just hook the callback function to the matplotlib draw event.

In any case, this solution is imperfect, but it should work in most situations. I'm not trying to take into account tex-rendering strings, any stretched fonts or fonts with an unusual aspect ratio. However, now it should correctly handle rotated text.

However, he should try to automatically wrap any text objects in several subheadings depending on what numbers you connect with the on_draw to ... In many cases, this will be imperfect, but it does a decent job.

 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() 

Figure with wrapped text

+103
Oct. 30 '10 at 1:24
source share

He was about five years old, but it seems like a great way to do this. Here is my version of the decision. My goal was to allow selective packaging with a pixel effect to be selectively applied to individual copies of the text. I also created a simple textBox () function that converts any axis into a text field with custom fields and alignment.

Instead of assuming a specific aspect ratio of the font or average width, I actually draw a line one word at a time and insert new lines after reaching the threshold. This is terribly slow compared to approximations, but still seems pretty fast for 200-word strings.

 # 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 

Using:

enter image description here

 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) 

I discarded several functions that were not so important to me. Resizing will fail, because each call to the _wrap () function inserts additional newline characters into the string, but cannot delete them. This can be resolved by removing all \ n characters in the _wrap function, or saving the original string somewhere and "dumping" the text instance between the wrappers.

+4
Nov 18 '15 at 9:11
source share



All Articles