Czy można wyświetlać tekst w pudełku za pośrednictwem Matplotlib, z automatycznymi łamaniami linii? Używając pyplot.text()
, mogłem drukować tylko tekst wielowierszowy, który przepływa poza granice okna, co jest denerwujące. Rozmiar linii nie jest znany z góry ... Każdy pomysł byłby mile widziany!Pole tekstowe z zawijaniem linii w matplotlib?
Odpowiedz
Zawartość tej odpowiedzi została scalona z wzorcem mpl w https://github.com/matplotlib/matplotlib/pull/4342 i będzie dostępna w następnym wydaniu funkcji.
Wow ... To drażliwy problem, ... (I naraża wiele ograniczeń w renderowania tekstu matplotlib za ...)
ta powinna (imo) być coś, co matplotlib ma wbudowany -in, ale tak nie jest. Na liście adresowej było kilka threads about it, ale nie znalazłem rozwiązania, które mogłoby pomóc w automatycznym zawijaniu tekstu.
Po pierwsze, nie ma możliwości określenia rozmiaru (w pikselach) renderowanego ciągu tekstowego przed jego narysowaniem w matplotlib. Nie jest to zbyt duży problem, ponieważ możemy go po prostu narysować, pobrać rozmiar, a następnie przerysować zawijany tekst. (Jest kosztowny, ale niezbyt przesadnie zły)
Następnym problemem jest to, że znaki nie mają stałej szerokości w pikselach, więc zawijanie łańcucha tekstowego do podanej liczby znaków nie musi odzwierciedlać danej szerokości, gdy renderowane. To jednak nie jest duży problem.
Co więcej, nie możemy tego zrobić po prostu raz ... W przeciwnym razie będzie on prawidłowo zawijany podczas rysowania za pierwszym razem (na przykład na ekranie), ale nie będzie, jeśli zostanie narysowany ponownie (gdy wielkość zostanie zmniejszona) lub zapisane jako obraz z inną rozdzielczością niż na ekranie). To nie jest duży problem, ponieważ możemy po prostu podłączyć funkcję wywołania zwrotnego do zdarzenia losowania matplotlib.
W każdym razie to rozwiązanie jest niedoskonałe, ale powinno działać w większości sytuacji. Nie próbuję uwzględniać tex-renderowanych ciągów, żadnych rozciągniętych czcionek ani czcionek o niezwykłym współczynniku kształtu. Powinien jednak poprawnie obsłużyć obrócony tekst.
Należy jednak spróbować automatycznie zawijać dowolne obiekty tekstowe na wielu podpikatach w zależności od tego, które liczby łączą się z oddzwanianiem na numer on_draw
... W wielu przypadkach będzie to niedoskonałe, ale przyzwoite.
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()
Jej były około pięciu lat, lecz wciąż nie wydaje się być świetnym sposobem, aby to zrobić. Oto moja wersja zaakceptowanego rozwiązania. Moim celem było umożliwienie selektywnego zawijania pikseli do poszczególnych instancji tekstowych. Stworzyłem również prostą funkcję textBox(), która przekształci dowolne osie w pole tekstowe z niestandardowymi marginesami i wyrównaniem.
Zamiast przyjmować określony współczynnik proporcji czcionki lub średnią szerokość, faktycznie narysuję ciąg po jednym słowie na raz i wstawię nowe linie po osiągnięciu progu. Jest to horrendalnie powolne w porównaniu z przybliżeniami, ale wciąż wydaje się dość żwawe dla ciągów o długości 200 słów.
# 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
Zastosowanie:
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)
rzuciłem kilka cech, które nie były tak ważne dla mnie. Zmiana rozmiaru nie powiedzie się, ponieważ każde wywołanie funkcji _wrap() wstawia dodatkowe znaki nowej linii do łańcucha, ale nie ma możliwości ich usunięcia. Można to rozwiązać, usuwając wszystkie \ n znaków z funkcji _wrap lub przechowując oryginalny ciąg gdzieś i "resetując" instancję tekstową między opcjami.
- 1. automatycznie ustawić pole tekstowe w matplotlib
- 2. Pole tekstowe z "nową linią"
- 3. Pole tekstowe z sugestiami rozwijanymi
- 4. Arabskie pole tekstowe
- 5. Niereferowalne pole tekstowe
- 6. Nieeksportowalne pole tekstowe wxPython
- 7. Pole tekstowe - centrowanie tekstu
- 8. Numeryczne pole tekstowe z wzorcem MVVM
- 9. Pole tekstowe z zaokrąglonymi narożnikami WPF
- 10. Jak utworzyć pole tekstowe tylko z JavaScript?
- 11. Zaktualizuj pole tekstowe podczas pisania
- 12. Proste jQuery - ukryj pole tekstowe
- 13. asp.net Walidator na pole tekstowe
- 14. ValidationRule dla WPF pole tekstowe
- 15. Google Suggestish pole tekstowe (autouzupełnianie)
- 16. Pole wektorowe 3D w matplotlib
- 17. Automatycznie wypełnij pole tekstowe asp.net
- 18. Pole tekstowe NumberFormat bez przecinków
- 19. Problemy z zawijaniem wierszy z powłoką IPython
- 20. Zwiększ szerokość linii linii legendy w matplotlib
- 21. Jak utworzyć pole tekstowe numeryczne w Silverlight?
- 22. Jak ustawić pole tekstowe w kodzie?
- 23. Jak mogę init, pole tekstowe w UIAlertView
- 24. Windows Phone Multi Line Pole tekstowe
- 25. Waliduj numeryczne pole tekstowe w jQuery
- 26. Reklamowe pole tekstowe jak w liczbach
- 27. Android drawText z zawijaniem tekstu
- 28. Jak ustawić pole tekstowe jako pole wyboru w django
- 29. Jak ustawić pole tekstowe ElementTree w konstruktorze?
- 30. Ustaw fokus na pole tekstowe w WPF
+1. Łał! Imponujące opanowanie Matplotlib. :) Z podanym kodem, kiedy zmieniam rozmiar okna, szerokości stają się mniejsze i mniejsze, ale wydają się nigdy już nie być większe (w tym osiągnięcie oryginalnego rozmiaru po przywróceniu oryginalnego rozmiaru okna) ... – EOL
@Joe: Wątek, który wskazujesz, jest również interesujący: opakowanie LaTeX może być przydatną opcją. – EOL
@EOL - Dzięki! Dodałem nową wersję, która rozwiązuje problemy z rozmiarem (a także poprawnie obsługuje tekst wyrównany do środka). Tekst powinien teraz ponownie przepływać, gdy liczba jest większa i mniejsza. Owijanie LaTeXa jest dobrą opcją (i zdecydowanie prostsze!), Ale nie mogę znaleźć sposobu na automatyczne dopasowanie rozmiaru osi ... Może brakuje mi czegoś oczywistego? –