¿Es posible mostrar el texto en un cuadro a través de Matplotlib, con saltos de línea automáticos? Al usar pyplot.text()
, solo pude imprimir texto de varias líneas que fluye más allá de los límites de la ventana, lo cual es molesto. El tamaño de las líneas no se conoce de antemano ... ¡Cualquier idea sería muy apreciada!Cuadro de texto con envoltura de línea en matplotlib?
Respuesta
El contenido de esta respuesta se fusionó en mpl master en https://github.com/matplotlib/matplotlib/pull/4342 y estará en la próxima versión de la función.
Wow ... Esto es un problema espinoso ... (Y se expone a una gran cantidad de limitaciones en la representación de texto de matplotlib ...)
Esto debe (OMI) matplotlib ser algo que se ha construido -in, pero no es así. Ha habido unos pocos threads about it en la lista de correo, pero no hay una solución que pueda encontrar para el ajuste de texto automático.
Así que, en primer lugar, no hay forma de determinar el tamaño (en píxeles) de la cadena de texto procesada antes de dibujarla en matplotlib. Esto no es un gran problema, ya que podemos dibujarlo, obtener el tamaño y luego volver a dibujar el texto envuelto. (Es caro, pero no excesivamente malo)
El siguiente problema es que los caracteres no tienen un ancho fijo en píxeles, por lo que envolver una cadena de texto con un número dado de caracteres no necesariamente reflejará un ancho determinado cuando prestado Este no es un gran problema, sin embargo.
Más allá de eso, no podemos hacer esto una sola vez ... De lo contrario, se ajustará correctamente cuando se dibuje la primera vez (en la pantalla, por ejemplo), pero no si se dibuja de nuevo (cuando se cambia el tamaño de la figura) o guardado como una imagen con un DPI diferente al de la pantalla). Esto no es un gran problema, ya que podemos simplemente conectar una función de devolución de llamada al evento de sorteo matplotlib.
En cualquier caso, esta solución es imperfecta, pero debería funcionar en la mayoría de las situaciones. No trato de dar cuenta de las cadenas representadas por tex, las fuentes estiradas o las fuentes con una relación de aspecto inusual. Sin embargo, ahora debería manejar adecuadamente el texto girado.
Sin embargo, debe intentar envolver automáticamente cualquier objeto de texto en múltiples subtramas en las cifras a las que conecte la devolución de llamada on_draw
... Será imperfecto en muchos casos, pero hace un trabajo decente.
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()
Su sido alrededor de cinco años, pero todavía no parece ser una gran manera de hacer esto. Aquí está mi versión de la solución aceptada. Mi objetivo era permitir que el ajuste perfecto de píxeles se aplicara de forma selectiva a instancias de texto individuales. También he creado una función simple textBox() que convertirá cualquier eje en un cuadro de texto con márgenes y alineación personalizados.
En lugar de asumir una relación de aspecto de fuente particular o un ancho promedio, en realidad dibujo la cadena una palabra a la vez e inserto líneas nuevas una vez que se alcanza el umbral. Esto es tremendamente lento en comparación con las aproximaciones, pero todavía se siente bastante ágil para cadenas de < 200 palabras.
# 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
Uso:
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)
me dio algunas características que no eran tan importante para mí. El cambio de tamaño fallará ya que cada llamada a _wrap() inserta nuevas líneas adicionales en la cadena, pero no tiene forma de eliminarlas. Esto puede resolverse eliminando todos los caracteres \ n de la función _wrap, o almacenando la cadena original en alguna parte y "reiniciando" la instancia de texto entre wraps.
- 1. Envoltura de texto con CSS
- 2. Bloques en línea y envoltura de texto con CSS
- 3. Cuadro de texto con "nueva línea"
- 4. Envoltura de texto con altura máxima fija
- 5. coloca automáticamente el cuadro de texto en matplotlib
- 6. PHPExcel y envoltura de texto
- 7. Leer cadena xml en cuadro de texto con nueva línea
- 8. Envoltura de texto con punto (graphviz)
- 9. convertir cuadro de texto a línea múltiple con jquery
- 10. envoltura larga de texto jqgrid
- 11. Envoltura de líneas de texto en JqGrid
- 12. Envoltura de un nodo de texto seleccionado con span
- 13. opción de envoltura de texto/ajuste de línea en Aptana Studio 3 Beta?
- 14. Subrayar texto en Python/Matplotlib
- 15. C# - Cuadro de texto de nueva línea Problemas
- 16. WPF toolkit datagrid texto de celda envoltura
- 17. Envoltura de texto Qml (ancho máximo)
- 18. Python: Matplotlib anota salto de línea (con y sin látex)
- 19. Cuadro de contraseña con texto de sugerencia
- 20. Línea de progreso en Matplotlib Gráficos
- 21. ¿Cómo guardo un cuadro de texto multilínea como una línea en un archivo de texto?
- 22. cómo anotar mapa de calor con texto en matplotlib?
- 23. texto fantasma - cómo tener en texto claro cuadro de texto
- 24. Línea de centrado: etiqueta de eje roto en matplotlib
- 25. Envoltura larga de texto en un lienzo de Android
- 26. Envoltura de texto de selección al adjuntar caracteres en Emacs
- 27. Envoltura de texto alrededor de una figura en LaTeX
- 28. Cuadro de texto numérico con patrón MVVM
- 29. Deshabilitar un cuadro de texto con CSS
- 30. ajuste de texto en el cuadro de
+1. ¡Guauu! Impresionante dominio de Matplotlib. :) Con el código que proporcionas, cuando cambio el tamaño de la ventana, el ancho se vuelve más pequeño y más pequeño, pero parece que nunca vuelve a agrandarse (incluso al alcanzar su tamaño original cuando la ventana vuelve a su tamaño original) ... – EOL
@Joe: El hilo que señala también es interesante: el ajuste LaTeX podría ser una opción útil. – EOL
@EOL - ¡Gracias! Agregué una nueva versión que corrige los problemas de cambio de tamaño (y también maneja el texto centrado correctamente). El texto ahora debe volver a fluir cuando la figura se hace más grande y más pequeña. La envoltura de LaTeX es una buena opción (¡y definitivamente más sencilla!), Pero parece que no puedo encontrar la manera de que se ajuste automáticamente al tamaño de los ejes ... ¿Tal vez me falta algo obvio? –