10
10
import math
11
11
import warnings
12
12
13
+ import contextlib
14
+
13
15
import numpy as np
14
16
15
17
from matplotlib import cbook
@@ -42,6 +44,22 @@ def _process_text_args(override, fontdict=None, **kwargs):
42
44
return override
43
45
44
46
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
+
45
63
# Extracted from Text's method to serve as a function
46
64
def get_rotation (rotation ):
47
65
"""
@@ -105,6 +123,7 @@ def get_rotation(rotation):
105
123
visible [True | False]
106
124
weight or fontweight ['normal' | 'bold' | 'heavy' | 'light' |
107
125
'ultrabold' | 'ultralight']
126
+ wrap [True | False]
108
127
x float
109
128
y float
110
129
zorder any number
@@ -175,6 +194,7 @@ def __init__(self,
175
194
linespacing = None ,
176
195
rotation_mode = None ,
177
196
usetex = None , # defaults to rcParams['text.usetex']
197
+ wrap = False ,
178
198
** kwargs
179
199
):
180
200
"""
@@ -198,6 +218,7 @@ def __init__(self,
198
218
self .set_text (text )
199
219
self .set_color (color )
200
220
self .set_usetex (usetex )
221
+ self .set_wrap (wrap )
201
222
self ._verticalalignment = verticalalignment
202
223
self ._horizontalalignment = horizontalalignment
203
224
self ._multialignment = multialignment
@@ -211,7 +232,7 @@ def __init__(self,
211
232
self ._linespacing = linespacing
212
233
self .set_rotation_mode (rotation_mode )
213
234
self .update (kwargs )
214
- #self.set_bbox(dict(pad=0))
235
+ # self.set_bbox(dict(pad=0))
215
236
216
237
def __getstate__ (self ):
217
238
d = super (Text , self ).__getstate__ ()
@@ -514,7 +535,7 @@ def update_bbox_position_size(self, renderer):
514
535
self ._bbox_patch .set_transform (tr )
515
536
fontsize_in_pixel = renderer .points_to_pixels (self .get_size ())
516
537
self ._bbox_patch .set_mutation_scale (fontsize_in_pixel )
517
- #self._bbox_patch.draw(renderer)
538
+ # self._bbox_patch.draw(renderer)
518
539
519
540
def _draw_bbox (self , renderer , posx , posy ):
520
541
@@ -587,6 +608,115 @@ def set_clip_on(self, b):
587
608
super (Text , self ).set_clip_on (b )
588
609
self ._update_clip_properties ()
589
610
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
+
590
720
@allow_rasterization
591
721
def draw (self , renderer ):
592
722
"""
@@ -601,56 +731,58 @@ def draw(self, renderer):
601
731
602
732
renderer .open_group ('text' , self .get_gid ())
603
733
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 ()
611
737
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 ))
614
742
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
646
777
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 )
654
786
655
787
gc .restore ()
656
788
renderer .close_group ('text' )
0 commit comments