問題描述
是否可以通過 Matplotlib 在框中顯示文本,自動換行?通過使用 pyplot.text()
,我只能打印超出窗口邊界的多行文本,這很煩人.事先不知道線條的大小......任何想法將不勝感激!
Is it possible to display text in a box through Matplotlib, with automatic line breaks? By using pyplot.text()
, I was only able to print multi-line text that flows beyond the boundaries of the window, which is annoying. The size of the lines is not known in advance… Any idea would be much appreciated!
推薦答案
這個答案的內容被合并到了https://github.com/matplotlib/matplotlib/pull/4342 并將在下一個功能版本中.
The contents of this answer were merged into mpl master in https://github.com/matplotlib/matplotlib/pull/4342 and will be in the next feature release.
哇...這是一個棘手的問題...(并且它暴露了matplotlib的文本渲染中的很多限制...)
Wow... This is a thorny problem... (And it exposes a lot of limitations in matplotlib's text rendering...)
這應該(i.m.o.)是 matplotlib 內置的東西,但事實并非如此.有一些 關于它的線程郵件列表,但我找不到自動換行的解決方案.
This should (i.m.o.) be something that matplotlib has built-in, but it doesn't. There have been a few threads about it on the mailing list, but no solution that I could find to automatic text wrapping.
因此,首先,在 matplotlib 中繪制之前,無法確定渲染文本字符串的大小(以像素為單位).這不是太大的問題,因為我們可以繪制它,獲取大小,然后重新繪制包裝的文本.(雖然很貴,但也不算太差)
So, first off, there's no way to determine the size (in pixels) of the rendered text string before it's drawn in matplotlib. This isn't too large of a problem, as we can just draw it, get the size, and then redraw the wrapped text. (It's expensive, but not too excessively bad)
下一個問題是字符沒有以像素為單位的固定寬度,因此將文本字符串包裝到給定數量的字符不一定會在渲染時反映給定的寬度.不過,這不是一個大問題.
The next problem is that characters don't have a fixed width in pixels, so wrapping a text string to a given number of characters won't necessarily reflect a given width when rendered. This isn't a huge problem, though.
除此之外,我們不能只做一次......否則,它會在第一次繪制時正確包裹(例如在屏幕上),但如果再次繪制(當圖形調整大小或保存為與屏幕具有不同 DPI 的圖像).這不是一個大問題,因為我們可以將回調函數連接到 matplotlib 繪制事件.
Beyond that, we can't just do this once... Otherwise, it will be wrapped correctly when drawn the first time (on the screen, for example), but not if drawn again (when the figure is resized or saved as an image with a different DPI than the screen). This isn't a huge problem, as we can just connect a callback function to the matplotlib draw event.
無論如何,這個解決方案是不完美的,但它應該適用于大多數情況.我不會嘗試考慮 tex 渲染的字符串、任何拉伸字體或具有不尋常縱橫比的字體.但是,它現在應該可以正確處理旋轉的文本.
At any rate this solution is imperfect, but it should work in most situations. I don't try to account for tex-rendered strings, any stretched fonts, or fonts with an unusual aspect ratio. However, it should now properly handle rotated text.
但是,它應該嘗試自動將任何文本對象包裝在多個子圖中,無論您將 on_draw
回調連接到哪個圖形...它在許多情況下都是不完美的,但它做得不錯.
However, It should attempt automatically wrap any text objects in multiple subplots in whichever figures you connect the on_draw
callback to... It will be imperfect in many cases, 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'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()
這篇關于matplotlib中換行的文本框?的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!