44
55from collections .abc import Sequence
66import functools
7+ import itertools
78import logging
89import math
910from numbers import Real
2425_log = logging .getLogger (__name__ )
2526
2627
28+ @functools .lru_cache (maxsize = 128 )
29+ def _rotate (theta ):
30+ """
31+ Return an Affine2D object that rotates by the given angle in radians.
32+ """
33+ return Affine2D ().rotate (theta )
34+
35+
36+ def _rotate_point (angle , x , y ):
37+ """
38+ Rotate point (x, y) by rotation angle in degrees
39+ """
40+ if angle == 0 :
41+ return (x , y )
42+ angle_rad = math .radians (angle )
43+ cos , sin = math .cos (angle_rad ), math .sin (angle_rad )
44+ return (cos * x - sin * y , sin * x + cos * y )
45+
46+
2747def _get_textbox (text , renderer ):
2848 """
2949 Calculate the bounding box of the text.
@@ -39,8 +59,8 @@ def _get_textbox(text, renderer):
3959
4060 projected_xys = []
4161
42- theta = np . deg2rad (text .get_rotation ())
43- tr = Affine2D (). rotate (- theta )
62+ theta = math . radians (text .get_rotation ())
63+ tr = _rotate (- theta )
4464
4565 _ , parts = text ._get_layout (renderer )
4666
@@ -57,7 +77,7 @@ def _get_textbox(text, renderer):
5777 xt_box , yt_box = min (projected_xs ), min (projected_ys )
5878 w_box , h_box = max (projected_xs ) - xt_box , max (projected_ys ) - yt_box
5979
60- x_box , y_box = Affine2D (). rotate (theta ).transform ((xt_box , yt_box ))
80+ x_box , y_box = _rotate (theta ).transform ((xt_box , yt_box ))
6181
6282 return x_box , y_box , w_box , h_box
6383
@@ -355,10 +375,10 @@ def _char_index_at(self, x):
355375 return (np .abs (size_accum - std_x )).argmin ()
356376
357377 def get_rotation (self ):
358- """Return the text angle in degrees between 0 and 360."""
378+ """Return the text angle in degrees in the range [0, 360) ."""
359379 if self .get_transform_rotates_text ():
360380 return self .get_transform ().transform_angles (
361- [self ._rotation ], [self .get_unitless_position ()]).item (0 )
381+ [self ._rotation ], [self .get_unitless_position ()]).item (0 ) % 360
362382 else :
363383 return self ._rotation
364384
@@ -496,9 +516,6 @@ def _get_layout(self, renderer):
496516 ymax = 0
497517 ymin = ys [- 1 ] - descent # baseline of last line minus its descent
498518
499- # get the rotation matrix
500- M = Affine2D ().rotate_deg (self .get_rotation ())
501-
502519 # now offset the individual text lines within the box
503520 malign = self ._get_multialignment ()
504521 if malign == 'left' :
@@ -511,16 +528,17 @@ def _get_layout(self, renderer):
511528 for x , y , w in zip (xs , ys , ws )]
512529
513530 # the corners of the unrotated bounding box
514- corners_horiz = np .array (
515- [(xmin , ymin ), (xmin , ymax ), (xmax , ymax ), (xmax , ymin )])
531+ corners_horiz = [(xmin , ymin ), (xmin , ymax ), (xmax , ymax ), (xmax , ymin )]
516532
517533 # now rotate the bbox
518- corners_rotated = M .transform (corners_horiz )
534+ angle = self .get_rotation ()
535+ rotate = functools .partial (_rotate_point , angle )
536+ corners_rotated = [rotate (x , y ) for x , y in corners_horiz ]
537+
519538 # compute the bounds of the rotated box
520- xmin = corners_rotated [:, 0 ].min ()
521- xmax = corners_rotated [:, 0 ].max ()
522- ymin = corners_rotated [:, 1 ].min ()
523- ymax = corners_rotated [:, 1 ].max ()
539+ xs , ys = zip (* corners_rotated )
540+ xmin , xmax = min (xs ), max (xs )
541+ ymin , ymax = min (ys ), max (ys )
524542 width = xmax - xmin
525543 height = ymax - ymin
526544
@@ -531,7 +549,6 @@ def _get_layout(self, renderer):
531549
532550 rotation_mode = self .get_rotation_mode ()
533551 if rotation_mode != "anchor" :
534- angle = self .get_rotation ()
535552 if rotation_mode == 'xtick' :
536553 halign = self ._ha_for_angle (angle )
537554 elif rotation_mode == 'ytick' :
@@ -577,15 +594,16 @@ def _get_layout(self, renderer):
577594 else :
578595 offsety = ymin1
579596
580- offsetx , offsety = M . transform (( offsetx , offsety ) )
597+ offsetx , offsety = rotate ( offsetx , offsety )
581598
582599 xmin -= offsetx
583600 ymin -= offsety
584601
585602 bbox = Bbox .from_bounds (xmin , ymin , width , height )
586603
587604 # now rotate the positions around the first (x, y) position
588- xys = M .transform (offset_layout ) - (offsetx , offsety )
605+ xys = [(x - offsetx , y - offsety )
606+ for x , y in itertools .starmap (rotate , offset_layout )]
589607
590608 return bbox , list (zip (lines , wads , xys ))
591609
@@ -726,11 +744,11 @@ def _get_wrap_line_width(self):
726744 # Calculate available width based on text alignment
727745 alignment = self .get_horizontalalignment ()
728746 self .set_rotation_mode ('anchor' )
729- rotation = self .get_rotation ()
747+ angle = self .get_rotation ()
730748
731- left = self ._get_dist_to_box (rotation , x0 , y0 , figure_box )
749+ left = self ._get_dist_to_box (angle , x0 , y0 , figure_box )
732750 right = self ._get_dist_to_box (
733- (180 + rotation ) % 360 , x0 , y0 , figure_box )
751+ (180 + angle ) % 360 , x0 , y0 , figure_box )
734752
735753 if alignment == 'left' :
736754 line_width = left
@@ -839,67 +857,65 @@ def draw(self, renderer):
839857
840858 renderer .open_group ('text' , self .get_gid ())
841859
842- with self ._cm_set (text = self ._get_wrapped_text ()):
843- bbox , info = self ._get_layout (renderer )
844- trans = self .get_transform ()
860+ bbox , info = self ._get_layout (renderer )
861+ trans = self .get_transform ()
862+
863+ # don't use self.get_position here, which refers to text
864+ # position in Text:
865+ x , y = self ._x , self ._y
866+ if np .ma .is_masked (x ):
867+ x = np .nan
868+ if np .ma .is_masked (y ):
869+ y = np .nan
870+ posx = float (self .convert_xunits (x ))
871+ posy = float (self .convert_yunits (y ))
872+ posx , posy = trans .transform ((posx , posy ))
873+ if np .isnan (posx ) or np .isnan (posy ):
874+ return # don't throw a warning here
875+ if not np .isfinite (posx ) or not np .isfinite (posy ):
876+ _log .warning ("posx and posy should be finite values" )
877+ return
878+ canvasw , canvash = renderer .get_canvas_width_height ()
845879
846- # don't use self.get_position here, which refers to text
847- # position in Text:
848- x , y = self ._x , self ._y
849- if np .ma .is_masked (x ):
850- x = np .nan
851- if np .ma .is_masked (y ):
852- y = np .nan
853- posx = float (self .convert_xunits (x ))
854- posy = float (self .convert_yunits (y ))
855- posx , posy = trans .transform ((posx , posy ))
856- if np .isnan (posx ) or np .isnan (posy ):
857- return # don't throw a warning here
858- if not np .isfinite (posx ) or not np .isfinite (posy ):
859- _log .warning ("posx and posy should be finite values" )
860- return
861- canvasw , canvash = renderer .get_canvas_width_height ()
862-
863- # Update the location and size of the bbox
864- # (`.patches.FancyBboxPatch`), and draw it.
865- if self ._bbox_patch :
866- self .update_bbox_position_size (renderer )
867- self ._bbox_patch .draw (renderer )
868-
869- gc = renderer .new_gc ()
870- gc .set_foreground (mcolors .to_rgba (self .get_color ()), isRGBA = True )
871- gc .set_alpha (self .get_alpha ())
872- gc .set_url (self ._url )
873- gc .set_antialiased (self ._antialiased )
874- gc .set_snap (self .get_snap ())
875- self ._set_gc_clip (gc )
876-
877- angle = self .get_rotation ()
878-
879- for line , wad , (x , y ) in info :
880-
881- mtext = self if len (info ) == 1 else None
882- x = x + posx
883- y = y + posy
884- if renderer .flipy ():
885- y = canvash - y
886- clean_line , ismath = self ._preprocess_math (line )
887-
888- if self .get_path_effects ():
889- from matplotlib .patheffects import PathEffectRenderer
890- textrenderer = PathEffectRenderer (
891- self .get_path_effects (), renderer )
892- else :
893- textrenderer = renderer
894-
895- if self .get_usetex ():
896- textrenderer .draw_tex (gc , x , y , clean_line ,
897- self ._fontproperties , angle ,
898- mtext = mtext )
899- else :
900- textrenderer .draw_text (gc , x , y , clean_line ,
901- self ._fontproperties , angle ,
902- ismath = ismath , mtext = mtext )
880+ # Update the location and size of the bbox
881+ # (`.patches.FancyBboxPatch`), and draw it.
882+ if self ._bbox_patch :
883+ self .update_bbox_position_size (renderer )
884+ self ._bbox_patch .draw (renderer )
885+
886+ gc = renderer .new_gc ()
887+ gc .set_foreground (mcolors .to_rgba (self .get_color ()), isRGBA = True )
888+ gc .set_alpha (self .get_alpha ())
889+ gc .set_url (self ._url )
890+ gc .set_antialiased (self ._antialiased )
891+ gc .set_snap (self .get_snap ())
892+ self ._set_gc_clip (gc )
893+
894+ angle = self .get_rotation ()
895+
896+ for line , wad , (x , y ) in info :
897+
898+ mtext = self if len (info ) == 1 else None
899+ x = x + posx
900+ y = y + posy
901+ if renderer .flipy ():
902+ y = canvash - y
903+ clean_line , ismath = self ._preprocess_math (line )
904+
905+ if self .get_path_effects ():
906+ from matplotlib .patheffects import PathEffectRenderer
907+ textrenderer = PathEffectRenderer (self .get_path_effects (), renderer )
908+ else :
909+ textrenderer = renderer
910+
911+ if self .get_usetex ():
912+ textrenderer .draw_tex (gc , x , y , clean_line ,
913+ self ._fontproperties , angle ,
914+ mtext = mtext )
915+ else :
916+ textrenderer .draw_text (gc , x , y , clean_line ,
917+ self ._fontproperties , angle ,
918+ ismath = ismath , mtext = mtext )
903919
904920 gc .restore ()
905921 renderer .close_group ('text' )
0 commit comments