1010import math
1111import warnings
1212
13+ import contextlib
14+
1315import numpy as np
1416
1517from 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
4664def 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