Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b8ea343

Browse files
committed
Merge pull request matplotlib#4342 from sptm/feature3418
ENH : Implement auto-wrapping text closes matplotlib#3418
2 parents 7f9fb69 + ada3f9a commit b8ea343

File tree

6 files changed

+244
-50
lines changed

6 files changed

+244
-50
lines changed

doc/users/whats_new/autowrap_text.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Auto-wrapping Text
2+
------------------
3+
Added the keyword argument "wrap" to Text, which automatically breaks long lines of text when being drawn.
4+
Works for any rotated text, different modes of alignment, and for text that are either labels or titles.
5+
6+
Example:
7+
plt.text(1, 1, "This is a really long string that should be wrapped so that it does not go outside the figure.", wrap=True)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
Auto-wrapping text demo.
3+
"""
4+
import matplotlib.pyplot as plt
5+
6+
fig = plt.figure()
7+
plt.axis([0, 10, 0, 10])
8+
t = "This is a really long string that I'd rather have wrapped so that it"\
9+
" doesn't go outside of the figure, but if it's long enough it will go"\
10+
" off the top or bottom!"
11+
plt.text(4, 1, t, ha='left', rotation=15, wrap=True)
12+
plt.text(6, 5, t, ha='left', rotation=15, wrap=True)
13+
plt.text(5, 5, t, ha='right', rotation=-15, wrap=True)
14+
plt.text(5, 10, t, fontsize=18, style='oblique', ha='center',
15+
va='top', wrap=True)
16+
plt.text(3, 4, t, family='serif', style='italic', ha='right', wrap=True)
17+
plt.text(-1, 0, t, ha='left', rotation=-15, wrap=True)
18+
19+
plt.show()

lib/matplotlib/tests/test_text.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,8 @@ def test_text_with_arrow_annotation_get_window_extent():
367367
headwidth = 21
368368
fig, ax = plt.subplots(dpi=100)
369369
txt = ax.text(s='test', x=0, y=0)
370-
ann = ax.annotate('test',
370+
ann = ax.annotate(
371+
'test',
371372
xy=(0.0, 50.0),
372373
xytext=(50.0, 50.0), xycoords='figure pixels',
373374
arrowprops={
@@ -441,3 +442,38 @@ def test_empty_annotation_get_window_extent():
441442
eq_(points[1, 0], 0.0)
442443
eq_(points[1, 1], 50.0)
443444
eq_(points[0, 1], 50.0)
445+
446+
447+
@image_comparison(baseline_images=['basictext_wrap'],
448+
extensions=['png'])
449+
def test_basic_wrap():
450+
fig = plt.figure()
451+
plt.axis([0, 10, 0, 10])
452+
t = "This is a really long string that I'd rather have wrapped so that" \
453+
" it doesn't go outside of the figure, but if it's long enough it" \
454+
" will go off the top or bottom!"
455+
plt.text(4, 1, t, ha='left', rotation=15, wrap=True)
456+
plt.text(6, 5, t, ha='left', rotation=15, wrap=True)
457+
plt.text(5, 5, t, ha='right', rotation=-15, wrap=True)
458+
plt.text(5, 10, t, fontsize=18, style='oblique', ha='center',
459+
va='top', wrap=True)
460+
plt.text(3, 4, t, family='serif', style='italic', ha='right', wrap=True)
461+
plt.text(-1, 0, t, ha='left', rotation=-15, wrap=True)
462+
463+
464+
@image_comparison(baseline_images=['fonttext_wrap'],
465+
extensions=['png'])
466+
def test_font_wrap():
467+
fig = plt.figure()
468+
plt.axis([0, 10, 0, 10])
469+
t = "This is a really long string that I'd rather have wrapped so that" \
470+
" it doesn't go outside of the figure, but if it's long enough it" \
471+
" will go off the top or bottom!"
472+
plt.text(4, -1, t, fontsize=18, family='serif', ha='left', rotation=15,
473+
wrap=True)
474+
plt.text(6, 5, t, family='sans serif', ha='left', rotation=15, wrap=True)
475+
plt.text(5, 5, t, weight='light', ha='right', rotation=-15, wrap=True)
476+
plt.text(5, 10, t, weight='heavy', ha='center', va='top', wrap=True)
477+
plt.text(3, 4, t, family='monospace', ha='right', wrap=True)
478+
plt.text(-1, 0, t, fontsize=14, style='italic', ha='left', rotation=-15,
479+
wrap=True)

lib/matplotlib/text.py

Lines changed: 181 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import math
1111
import warnings
1212

13+
import contextlib
14+
1315
import numpy as np
1416

1517
from matplotlib import cbook
@@ -42,6 +44,22 @@ def _process_text_args(override, fontdict=None, **kwargs):
4244
return override
4345

4446

47+
@contextlib.contextmanager
48+
def _wrap_text(textobj):
49+
"""
50+
Temporarily inserts newlines to the text if the wrap option is enabled.
51+
"""
52+
if textobj.get_wrap():
53+
old_text = textobj.get_text()
54+
try:
55+
textobj.set_text(textobj._get_wrapped_text())
56+
yield textobj
57+
finally:
58+
textobj.set_text(old_text)
59+
else:
60+
yield textobj
61+
62+
4563
# Extracted from Text's method to serve as a function
4664
def get_rotation(rotation):
4765
"""
@@ -105,6 +123,7 @@ def get_rotation(rotation):
105123
visible [True | False]
106124
weight or fontweight ['normal' | 'bold' | 'heavy' | 'light' |
107125
'ultrabold' | 'ultralight']
126+
wrap [True | False]
108127
x float
109128
y float
110129
zorder any number
@@ -175,6 +194,7 @@ def __init__(self,
175194
linespacing=None,
176195
rotation_mode=None,
177196
usetex=None, # defaults to rcParams['text.usetex']
197+
wrap=False,
178198
**kwargs
179199
):
180200
"""
@@ -198,6 +218,7 @@ def __init__(self,
198218
self.set_text(text)
199219
self.set_color(color)
200220
self.set_usetex(usetex)
221+
self.set_wrap(wrap)
201222
self._verticalalignment = verticalalignment
202223
self._horizontalalignment = horizontalalignment
203224
self._multialignment = multialignment
@@ -211,7 +232,7 @@ def __init__(self,
211232
self._linespacing = linespacing
212233
self.set_rotation_mode(rotation_mode)
213234
self.update(kwargs)
214-
#self.set_bbox(dict(pad=0))
235+
# self.set_bbox(dict(pad=0))
215236

216237
def __getstate__(self):
217238
d = super(Text, self).__getstate__()
@@ -514,7 +535,7 @@ def update_bbox_position_size(self, renderer):
514535
self._bbox_patch.set_transform(tr)
515536
fontsize_in_pixel = renderer.points_to_pixels(self.get_size())
516537
self._bbox_patch.set_mutation_scale(fontsize_in_pixel)
517-
#self._bbox_patch.draw(renderer)
538+
# self._bbox_patch.draw(renderer)
518539

519540
def _draw_bbox(self, renderer, posx, posy):
520541

@@ -587,6 +608,115 @@ def set_clip_on(self, b):
587608
super(Text, self).set_clip_on(b)
588609
self._update_clip_properties()
589610

611+
def get_wrap(self):
612+
"""
613+
Returns the wrapping state for the text.
614+
"""
615+
return self._wrap
616+
617+
def set_wrap(self, wrap):
618+
"""
619+
Sets the wrapping state for the text.
620+
"""
621+
self._wrap = wrap
622+
623+
def _get_wrap_line_width(self):
624+
"""
625+
Returns the maximum line width for wrapping text based on the
626+
current orientation.
627+
"""
628+
x0, y0 = self.get_transform().transform(self.get_position())
629+
figure_box = self.get_figure().get_window_extent()
630+
631+
# Calculate available width based on text alignment
632+
alignment = self.get_horizontalalignment()
633+
self.set_rotation_mode('anchor')
634+
rotation = self.get_rotation()
635+
636+
left = self._get_dist_to_box(rotation, x0, y0, figure_box)
637+
right = self._get_dist_to_box(
638+
(180 + rotation) % 360,
639+
x0,
640+
y0,
641+
figure_box)
642+
643+
if alignment == 'left':
644+
line_width = left
645+
elif alignment == 'right':
646+
line_width = right
647+
else:
648+
line_width = 2 * min(left, right)
649+
650+
return line_width
651+
652+
def _get_dist_to_box(self, rotation, x0, y0, figure_box):
653+
"""
654+
Returns the distance from the given points, to the boundaries
655+
of a rotated box in pixels.
656+
"""
657+
if rotation > 270:
658+
quad = rotation - 270
659+
h1 = y0 / math.cos(math.radians(quad))
660+
h2 = (figure_box.x1 - x0) / math.cos(math.radians(90 - quad))
661+
elif rotation > 180:
662+
quad = rotation - 180
663+
h1 = x0 / math.cos(math.radians(quad))
664+
h2 = y0 / math.cos(math.radians(90 - quad))
665+
elif rotation > 90:
666+
quad = rotation - 90
667+
h1 = (figure_box.y1 - y0) / math.cos(math.radians(quad))
668+
h2 = x0 / math.cos(math.radians(90 - quad))
669+
else:
670+
h1 = (figure_box.x1 - x0) / math.cos(math.radians(rotation))
671+
h2 = (figure_box.y1 - y0) / math.cos(math.radians(90 - rotation))
672+
673+
return min(h1, h2)
674+
675+
def _get_rendered_text_width(self, text):
676+
"""
677+
Returns the width of a given text string, in pixels.
678+
"""
679+
w, h, d = self._renderer.get_text_width_height_descent(
680+
text,
681+
self.get_fontproperties(),
682+
False)
683+
return math.ceil(w)
684+
685+
def _get_wrapped_text(self):
686+
"""
687+
Return a copy of the text with new lines added, so that
688+
the text is wrapped relative to the parent figure.
689+
"""
690+
# Not fit to handle breaking up latex syntax correctly, so
691+
# ignore latex for now.
692+
if self.get_usetex():
693+
return self.get_text()
694+
695+
# Build the line incrementally, for a more accurate measure of length
696+
line_width = self._get_wrap_line_width()
697+
wrapped_str = ""
698+
line = ""
699+
700+
for word in self.get_text().split(' '):
701+
# New lines in the user's test need to force a split, so that it's
702+
# not using the longest current line width in the line being built
703+
sub_words = word.split('\n')
704+
for i in range(len(sub_words)):
705+
current_width = self._get_rendered_text_width(
706+
line + ' ' + sub_words[i])
707+
708+
# Split long lines, and each newline found in the current word
709+
if current_width > line_width or i > 0:
710+
wrapped_str += line + '\n'
711+
line = ""
712+
713+
if line == "":
714+
line = sub_words[i]
715+
else:
716+
line += ' ' + sub_words[i]
717+
718+
return wrapped_str + line
719+
590720
@allow_rasterization
591721
def draw(self, renderer):
592722
"""
@@ -601,56 +731,58 @@ def draw(self, renderer):
601731

602732
renderer.open_group('text', self.get_gid())
603733

604-
bbox, info, descent = self._get_layout(renderer)
605-
trans = self.get_transform()
606-
607-
# don't use self.get_position here, which refers to text position
608-
# in Text, and dash position in TextWithDash:
609-
posx = float(self.convert_xunits(self._x))
610-
posy = float(self.convert_yunits(self._y))
734+
with _wrap_text(self) as textobj:
735+
bbox, info, descent = textobj._get_layout(renderer)
736+
trans = textobj.get_transform()
611737

612-
posx, posy = trans.transform_point((posx, posy))
613-
canvasw, canvash = renderer.get_canvas_width_height()
738+
# don't use textobj.get_position here, which refers to text
739+
# position in Text, and dash position in TextWithDash:
740+
posx = float(textobj.convert_xunits(textobj._x))
741+
posy = float(textobj.convert_yunits(textobj._y))
614742

615-
# draw the FancyBboxPatch
616-
if self._bbox_patch:
617-
self._draw_bbox(renderer, posx, posy)
618-
619-
gc = renderer.new_gc()
620-
gc.set_foreground(self.get_color())
621-
gc.set_alpha(self.get_alpha())
622-
gc.set_url(self._url)
623-
self._set_gc_clip(gc)
624-
625-
if self._bbox:
626-
bbox_artist(self, renderer, self._bbox)
627-
angle = self.get_rotation()
628-
629-
for line, wh, x, y in info:
630-
if not np.isfinite(x) or not np.isfinite(y):
631-
continue
632-
633-
mtext = self if len(info) == 1 else None
634-
x = x + posx
635-
y = y + posy
636-
if renderer.flipy():
637-
y = canvash - y
638-
clean_line, ismath = self.is_math_text(line)
639-
640-
if self.get_path_effects():
641-
from matplotlib.patheffects import PathEffectRenderer
642-
textrenderer = PathEffectRenderer(self.get_path_effects(),
643-
renderer)
644-
else:
645-
textrenderer = renderer
743+
posx, posy = trans.transform_point((posx, posy))
744+
canvasw, canvash = renderer.get_canvas_width_height()
745+
746+
# draw the FancyBboxPatch
747+
if textobj._bbox_patch:
748+
textobj._draw_bbox(renderer, posx, posy)
749+
750+
gc = renderer.new_gc()
751+
gc.set_foreground(textobj.get_color())
752+
gc.set_alpha(textobj.get_alpha())
753+
gc.set_url(textobj._url)
754+
textobj._set_gc_clip(gc)
755+
756+
if textobj._bbox:
757+
bbox_artist(textobj, renderer, textobj._bbox)
758+
angle = textobj.get_rotation()
759+
760+
for line, wh, x, y in info:
761+
if not np.isfinite(x) or not np.isfinite(y):
762+
continue
763+
764+
mtext = textobj if len(info) == 1 else None
765+
x = x + posx
766+
y = y + posy
767+
if renderer.flipy():
768+
y = canvash - y
769+
clean_line, ismath = textobj.is_math_text(line)
770+
771+
if textobj.get_path_effects():
772+
from matplotlib.patheffects import PathEffectRenderer
773+
textrenderer = PathEffectRenderer(
774+
textobj.get_path_effects(), renderer)
775+
else:
776+
textrenderer = renderer
646777

647-
if self.get_usetex():
648-
textrenderer.draw_tex(gc, x, y, clean_line,
649-
self._fontproperties, angle, mtext=mtext)
650-
else:
651-
textrenderer.draw_text(gc, x, y, clean_line,
652-
self._fontproperties, angle,
653-
ismath=ismath, mtext=mtext)
778+
if textobj.get_usetex():
779+
textrenderer.draw_tex(gc, x, y, clean_line,
780+
textobj._fontproperties, angle,
781+
mtext=mtext)
782+
else:
783+
textrenderer.draw_text(gc, x, y, clean_line,
784+
textobj._fontproperties, angle,
785+
ismath=ismath, mtext=mtext)
654786

655787
gc.restore()
656788
renderer.close_group('text')

0 commit comments

Comments
 (0)