22# Created: 23 Sep 2005
33# Parts rewritten by Reinier Heeres <reinier@heeres.eu>
44
5+ from dataclasses import dataclass
56import inspect
67
78import numpy as np
89
910import matplotlib as mpl
1011from matplotlib import (
1112 _api , artist , lines as mlines , axis as maxis , patches as mpatches ,
12- transforms as mtransforms , colors as mcolors )
13+ text as mtext , transforms as mtransforms , colors as mcolors )
1314from . import art3d , proj3d
1415
1516
@@ -35,6 +36,32 @@ def _tick_update_position(tick, tickxs, tickys, labelpos):
3536 tick .gridline .set_data ([0 ], [0 ])
3637
3738
39+ @dataclass
40+ class _UpdatedArtists :
41+ """
42+ Class to supply artists that have been updated ready to draw.
43+
44+ We track the types of artists individually, because they need
45+ different handling in the tightbox calculation.
46+ """
47+ line : mlines .Line2D = None
48+ ticks : list = None
49+ offset_text : mtext .Text = None
50+ label : mtext .Text = None
51+
52+ def __iter__ (self ):
53+ """Yield all updated artists in the correct order for drawing"""
54+ if self .line is not None :
55+ yield self .line
56+ if self .ticks is not None :
57+ for tick in self .ticks :
58+ yield tick
59+ if self .offset_text is not None :
60+ yield self .offset_text
61+ if self .label is not None :
62+ yield self .label
63+
64+
3865class Axis (maxis .XAxis ):
3966 """An Axis class for the 3D plots."""
4067 # These points from the unit cube make up the x, y and z-planes
@@ -439,8 +466,9 @@ def _axmask(self):
439466 axmask [self ._axinfo ["i" ]] = False
440467 return axmask
441468
442- def _draw_ticks (self , renderer , edgep1 , centers , deltas , highs ,
443- deltas_per_point , pos ):
469+ def _get_updated_ticks (self , edgep1 , centers , deltas , highs ,
470+ deltas_per_point , pos ):
471+ """Update the ticks ready for draw and return in a list."""
444472 ticks = self ._update_ticks ()
445473 info = self ._axinfo
446474 index = info ["i" ]
@@ -453,7 +481,7 @@ def _draw_ticks(self, renderer, edgep1, centers, deltas, highs,
453481 axis = [self .axes .xaxis , self .axes .yaxis , self .axes .zaxis ][index ]
454482 axis_trans = axis .get_transform ()
455483
456- # Draw ticks:
484+ # Update ticks:
457485 tickdir = self ._get_tickdir (pos )
458486 tickdelta = deltas [tickdir ] if highs [tickdir ] else - deltas [tickdir ]
459487
@@ -485,10 +513,12 @@ def _draw_ticks(self, renderer, edgep1, centers, deltas, highs,
485513
486514 _tick_update_position (tick , (x1 , x2 ), (y1 , y2 ), (lx , ly ))
487515 tick .tick1line .set_linewidth (tick_lw [tick ._major ])
488- tick .draw (renderer )
489516
490- def _draw_offset_text (self , renderer , edgep1 , edgep2 , labeldeltas , centers ,
491- highs , pep , dx , dy ):
517+ return ticks
518+
519+ def _update_offset_text (self , edgep1 , edgep2 , labeldeltas , centers ,
520+ highs , pep , dx , dy ):
521+ """Update the offset text ready to draw."""
492522 # Get general axis information:
493523 info = self ._axinfo
494524 index = info ["i" ]
@@ -556,12 +586,11 @@ def _draw_offset_text(self, renderer, edgep1, edgep2, labeldeltas, centers,
556586
557587 self .offsetText .set_va ('center' )
558588 self .offsetText .set_ha (align )
559- self .offsetText .draw (renderer )
560589
561- def _draw_labels (self , renderer , edgep1 , edgep2 , labeldeltas , centers , dx , dy ):
590+ def _update_label_position (self , edgep1 , edgep2 , labeldeltas , centers , dx , dy ):
591+ """Update the axis label position ready to draw."""
562592 label = self ._axinfo ["label" ]
563593
564- # Draw labels
565594 lxyz = 0.5 * (edgep1 + edgep2 )
566595 lxyz = _move_from_center (lxyz , centers , labeldeltas , self ._axmask ())
567596 tlx , tly , tlz = proj3d .proj_transform (* lxyz , self .axes .M )
@@ -572,13 +601,26 @@ def _draw_labels(self, renderer, edgep1, edgep2, labeldeltas, centers, dx, dy):
572601 self .label .set_va (label ['va' ])
573602 self .label .set_ha (label ['ha' ])
574603 self .label .set_rotation_mode (label ['rotation_mode' ])
575- self .label .draw (renderer )
576604
577- @artist .allow_rasterization
578- def draw (self , renderer ):
605+ def _update_all_positions (self ):
606+ """
607+ Update positions of all child artists ready to draw. Artists may be drawn
608+ twice: if tick_position is 'both', the ticks, line and offset text show on both
609+ sides of the axes; if label_position is 'both' the label shows on both sides.
610+ Since tightbox calculation needs individual handling of the different Artist
611+ types, we return them through the dataclass `_UpdatedArtists` (as opposed to a
612+ plain list).
613+ Since we reuse the same artists with different positions, the calling function
614+ requires them after every position update, so we yield `_UpdatedArtists` up to
615+ four times with different content:
616+
617+ - line, ticks, offset_text on one side
618+ - line, ticks, offset_text on the other side
619+ - label on one side
620+ - label on the other side
621+ """
579622 self .label ._transform = self .axes .transData
580623 self .offsetText ._transform = self .axes .transData
581- renderer .open_group ("axis3d" , gid = self .get_gid ())
582624
583625 # Get general axis information:
584626 mins , maxs , tc , highs = self ._get_coord_info ()
@@ -613,28 +655,42 @@ def draw(self, renderer):
613655 dx , dy = (self .axes .transAxes .transform ([pep [0 :2 , 1 ]]) -
614656 self .axes .transAxes .transform ([pep [0 :2 , 0 ]]))[0 ]
615657
616- # Draw the lines
658+ # Handle the lines
617659 self .line .set_data (pep [0 ], pep [1 ])
618- self .line .draw (renderer )
619660
620- # Draw ticks
621- self ._draw_ticks ( renderer , edgep1 , centers , deltas , highs ,
622- deltas_per_point , pos )
661+ # Handle ticks
662+ ticks = self ._get_updated_ticks (
663+ edgep1 , centers , deltas , highs , deltas_per_point , pos )
623664
624- # Draw Offset text
625- self ._draw_offset_text ( renderer , edgep1 , edgep2 , labeldeltas ,
626- centers , highs , pep , dx , dy )
665+ # Handle Offset text
666+ self ._update_offset_text (
667+ edgep1 , edgep2 , labeldeltas , centers , highs , pep , dx , dy )
627668
628- for edgep1 , edgep2 , pos in zip (* self ._get_all_axis_line_edge_points (
629- minmax , maxmin , self ._label_position )):
630- # See comments above
631- pep = proj3d ._proj_trans_points ([edgep1 , edgep2 ], self .axes .M )
632- pep = np .asarray (pep )
633- dx , dy = (self .axes .transAxes .transform ([pep [0 :2 , 1 ]]) -
634- self .axes .transAxes .transform ([pep [0 :2 , 0 ]]))[0 ]
669+ yield _UpdatedArtists (line = self .line , ticks = ticks ,
670+ offset_text = self .offsetText )
671+
672+ if self .label .get_visible () and self .label .get_text ():
673+ # Handle label
674+ for edgep1 , edgep2 , pos in zip (* self ._get_all_axis_line_edge_points (
675+ minmax , maxmin , self ._label_position )):
676+ # See comments above
677+ pep = proj3d ._proj_trans_points ([edgep1 , edgep2 ], self .axes .M )
678+ pep = np .asarray (pep )
679+ dx , dy = (self .axes .transAxes .transform ([pep [0 :2 , 1 ]]) -
680+ self .axes .transAxes .transform ([pep [0 :2 , 0 ]]))[0 ]
681+
682+ self ._update_label_position (
683+ edgep1 , edgep2 , labeldeltas , centers , dx , dy )
684+
685+ yield _UpdatedArtists (label = self .label )
686+
687+ @artist .allow_rasterization
688+ def draw (self , renderer ):
689+ renderer .open_group ('axis3d' , gid = self .get_gid ())
635690
636- # Draw labels
637- self ._draw_labels (renderer , edgep1 , edgep2 , labeldeltas , centers , dx , dy )
691+ for updated_artists in self ._update_all_positions ():
692+ for art in updated_artists :
693+ art .draw (renderer )
638694
639695 renderer .close_group ('axis3d' )
640696 self .stale = False
@@ -689,49 +745,32 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False):
689745 # docstring inherited
690746 if not self .get_visible ():
691747 return
692- # We have to directly access the internal data structures
693- # (and hope they are up to date) because at draw time we
694- # shift the ticks and their labels around in (x, y) space
695- # based on the projection, the current view port, and their
696- # position in 3D space. If we extend the transforms framework
697- # into 3D we would not need to do this different book keeping
698- # than we do in the normal axis
699- major_locs = self .get_majorticklocs ()
700- minor_locs = self .get_minorticklocs ()
701-
702- ticks = [* self .get_minor_ticks (len (minor_locs )),
703- * self .get_major_ticks (len (major_locs ))]
704- view_low , view_high = self .get_view_interval ()
705- if view_low > view_high :
706- view_low , view_high = view_high , view_low
707- interval_t = self .get_transform ().transform ([view_low , view_high ])
708-
709- ticks_to_draw = []
710- for tick in ticks :
711- try :
712- loc_t = self .get_transform ().transform (tick .get_loc ())
713- except AssertionError :
714- # Transform.transform doesn't allow masked values but
715- # some scales might make them, so we need this try/except.
716- pass
717- else :
718- if mtransforms ._interval_contains_close (interval_t , loc_t ):
719- ticks_to_draw .append (tick )
720748
721- ticks = ticks_to_draw
749+ if self .axes .M is None :
750+ self .axes .M = self .axes .get_proj ()
751+ self .axes .invM = np .linalg .inv (self .axes .M )
752+
753+ bboxes = []
722754
723- bb_1 , bb_2 = self ._get_ticklabel_bboxes (ticks , renderer )
724- other = []
755+ for updated_artists in self ._update_all_positions ():
756+ if (ticks := updated_artists .ticks ) is not None :
757+ for tlabel_bbs in self ._get_ticklabel_bboxes (ticks , renderer ):
758+ bboxes .extend (tlabel_bbs )
725759
726- if self .offsetText .get_visible () and self .offsetText .get_text ():
727- other .append (self .offsetText .get_window_extent (renderer ))
728- if self .line .get_visible ():
729- other .append (self .line .get_window_extent (renderer ))
730- if (self .label .get_visible () and not for_layout_only and
731- self .label .get_text ()):
732- other .append (self .label .get_window_extent (renderer ))
760+ if (line := updated_artists .line ) is not None and line .get_visible ():
761+ bboxes .append (line .get_window_extent (renderer ))
733762
734- return mtransforms .Bbox .union ([* bb_1 , * bb_2 , * other ])
763+ if ((offset_text := updated_artists .offset_text ) is not None and
764+ offset_text .get_visible () and offset_text .get_text ()):
765+ bboxes .append (offset_text .get_window_extent (renderer ))
766+
767+ if updated_artists .label is not None and not for_layout_only :
768+ bboxes .append (updated_artists .label .get_window_extent (renderer ))
769+
770+ if bboxes :
771+ return mtransforms .Bbox .union (bboxes )
772+ else :
773+ return None
735774
736775 d_interval = _api .deprecated (
737776 "3.6" , alternative = "get_data_interval" , pending = True )(
0 commit comments