1+ import itertools
2+ from typing import Type , List
13from matplotlib .axes import Axes
24from matplotlib .transforms import Bbox
35import matplotlib .docstring as docstring
46from matplotview ._transform_renderer import _TransformRenderer
5-
6-
7- def view_wrapper (axes_class ):
7+ from matplotlib .artist import Artist
8+ from matplotlib .backend_bases import RendererBase
9+
10+ class BoundRendererArtist :
11+ def __init__ (self , artist : Artist , renderer : RendererBase , clip_box : Bbox ):
12+ self ._artist = artist
13+ self ._renderer = renderer
14+ self ._clip_box = clip_box
15+
16+ def __getattribute__ (self , item ):
17+ try :
18+ return super ().__getattribute__ (item )
19+ except AttributeError :
20+ return self ._artist .__getattribute__ (item )
21+
22+ def __setattr__ (self , key , value ):
23+ try :
24+ super ().__setattr__ (key , value )
25+ except AttributeError :
26+ self ._artist .__setattr__ (key , value )
27+
28+ def draw (self , renderer : RendererBase ):
29+ # Disable the artist defined clip box, as the artist might be visible
30+ # under the new renderer even if not on screen...
31+ clip_box_orig = self ._artist .get_clip_box ()
32+ full_extents = self ._artist .get_window_extent (self ._renderer )
33+ self ._artist .set_clip_box (full_extents )
34+
35+ # Check and see if the passed limiting box and extents of the
36+ # artist intersect, if not don't bother drawing this artist.
37+ if (Bbox .intersection (full_extents , self ._clip_box ) is not None ):
38+ self ._artist .draw (self ._renderer )
39+
40+ # Re-enable the clip box...
41+ self ._artist .set_clip_box (clip_box_orig )
42+
43+
44+ def view_wrapper (axes_class : Type [Axes ]) -> Type [Axes ]:
845 """
946 Construct a ViewAxes, which subclasses, or wraps a specific Axes subclass.
1047 A ViewAxes can be configured to display the contents of another Axes
@@ -30,13 +67,13 @@ class ViewAxesImpl(axes_class):
3067 """
3168 __module__ = axes_class .__module__
3269 # The number of allowed recursions in the draw method
33- MAX_RENDER_DEPTH = 1
70+ MAX_RENDER_DEPTH = 5
3471
3572 def __init__ (
3673 self ,
37- axes_to_view ,
74+ axes_to_view : Axes ,
3875 * args ,
39- image_interpolation = "nearest" ,
76+ image_interpolation : str = "nearest" ,
4077 ** kwargs
4178 ):
4279 """
@@ -70,90 +107,68 @@ def __init__(
70107 ViewAxes
71108 The new zoom view axes instance...
72109 """
73- super ().__init__ (axes_to_view .figure , * args , zorder = zorder ,
74- ** kwargs )
110+ super ().__init__ (axes_to_view .figure , * args , ** kwargs )
75111 self ._init_vars (axes_to_view , image_interpolation )
76112
77-
78113 def _init_vars (
79114 self ,
80- axes_to_view ,
81- image_interpolation = "nearest"
115+ axes_to_view : Axes ,
116+ image_interpolation : str = "nearest"
82117 ):
83118 self .__view_axes = axes_to_view
84119 self .__image_interpolation = image_interpolation
85120 self ._render_depth = 0
86121 self .__scale_lines = True
87-
88- def draw (self , renderer = None ):
122+ self .__renderer = None
123+
124+ def get_children (self ) -> List [Artist ]:
125+ # We overload get_children to return artists from the view axes
126+ # in addition to this axes when drawing. We wrap the artists
127+ # in a BoundRendererArtist, so they are drawn with an alternate
128+ # renderer, and therefore to the correct location.
129+ if (self .__renderer is not None ):
130+ mock_renderer = _TransformRenderer (
131+ self .__renderer , self .__view_axes .transData ,
132+ self .transData , self , self .__image_interpolation ,
133+ self .__scale_lines
134+ )
135+
136+ x1 , x2 = self .get_xlim ()
137+ y1 , y2 = self .get_ylim ()
138+ axes_box = Bbox .from_extents (x1 , y1 , x2 , y2 ).transformed (
139+ self .__view_axes .transData
140+ )
141+
142+ init_list = super ().get_children ()
143+ init_list .extend ([
144+ BoundRendererArtist (a , mock_renderer , axes_box )
145+ for a in itertools .chain (
146+ self .__view_axes ._children , self .__view_axes .child_axes
147+ ) if (a is not self )
148+ ])
149+
150+ return init_list
151+ else :
152+ return super ().get_children ()
153+
154+ def draw (self , renderer : RendererBase = None ):
155+ # It is possible to have two axes which are views of each other
156+ # therefore we track the number of recursions and stop drawing
157+ # at a certain depth
89158 if (self ._render_depth >= self .MAX_RENDER_DEPTH ):
90159 return
91160 self ._render_depth += 1
161+ # Set the renderer, causing get_children to return the view's
162+ # children also...
163+ self .__renderer = renderer
92164
93165 super ().draw (renderer )
94166
95- if (not self .get_visible ()):
96- return
97-
98- axes_children = [
99- * self .__view_axes .collections ,
100- * self .__view_axes .patches ,
101- * self .__view_axes .lines ,
102- * self .__view_axes .texts ,
103- * self .__view_axes .artists ,
104- * self .__view_axes .images ,
105- * self .__view_axes .child_axes
106- ]
107-
108- # Sort all rendered items by their z-order so they render in layers
109- # correctly...
110- axes_children .sort (key = lambda obj : obj .get_zorder ())
111-
112- artist_boxes = []
113- # We need to temporarily disable the clip boxes of all of the
114- # artists, in order to allow us to continue rendering them it even
115- # if it is outside of the parent axes (they might still be visible
116- # in this zoom axes).
117- for a in axes_children :
118- artist_boxes .append (a .get_clip_box ())
119- a .set_clip_box (a .get_window_extent (renderer ))
120-
121- # Construct mock renderer and draw all artists to it.
122- mock_renderer = _TransformRenderer (
123- renderer , self .__view_axes .transData , self .transData , self ,
124- self .__image_interpolation , self .__scale_lines
125- )
126- x1 , x2 = self .get_xlim ()
127- y1 , y2 = self .get_ylim ()
128- axes_box = Bbox .from_extents (x1 , y1 , x2 , y2 ).transformed (
129- self .__view_axes .transData
130- )
131-
132- for artist in axes_children :
133- if (
134- (artist is not self )
135- and (
136- Bbox .intersection (
137- artist .get_window_extent (renderer ), axes_box
138- ) is not None
139- )
140- ):
141- artist .draw (mock_renderer )
142-
143- # Reset all of the artist clip boxes...
144- for a , box in zip (axes_children , artist_boxes ):
145- a .set_clip_box (box )
146-
147- # We need to redraw the splines if enabled, as we have finally
148- # drawn everything... This avoids other objects being drawn over
149- # the splines.
150- if (self .axison and self ._frameon ):
151- for spine in self .spines .values ():
152- spine .draw (renderer )
153-
167+ # Get rid of the renderer...
168+ self .__renderer = None
154169 self ._render_depth -= 1
155170
156- def get_linescaling (self ):
171+ def get_linescaling (self ) -> bool :
157172 """
158173 Get if line width scaling is enabled.
159174
@@ -164,7 +179,7 @@ def get_linescaling(self):
164179 """
165180 return self .__scale_lines
166181
167- def set_linescaling (self , value ):
182+ def set_linescaling (self , value : bool ):
168183 """
169184 Set whether line widths should be scaled when rendering a view of
170185 an axes.
@@ -178,7 +193,12 @@ def set_linescaling(self, value):
178193 self .__scale_lines = value
179194
180195 @classmethod
181- def from_axes (cls , axes , axes_to_view , image_interpolation = "nearest" ):
196+ def from_axes (
197+ cls ,
198+ axes : Axes ,
199+ axes_to_view : Axes ,
200+ image_interpolation : str = "nearest"
201+ ):
182202 axes .__class__ = cls
183203 axes ._init_vars (axes_to_view , image_interpolation )
184204 return axes
0 commit comments