1
+ import itertools
2
+ from typing import Type , List
1
3
from matplotlib .axes import Axes
2
4
from matplotlib .transforms import Bbox
3
5
import matplotlib .docstring as docstring
4
6
from 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 ]:
8
45
"""
9
46
Construct a ViewAxes, which subclasses, or wraps a specific Axes subclass.
10
47
A ViewAxes can be configured to display the contents of another Axes
@@ -30,13 +67,13 @@ class ViewAxesImpl(axes_class):
30
67
"""
31
68
__module__ = axes_class .__module__
32
69
# The number of allowed recursions in the draw method
33
- MAX_RENDER_DEPTH = 1
70
+ MAX_RENDER_DEPTH = 5
34
71
35
72
def __init__ (
36
73
self ,
37
- axes_to_view ,
74
+ axes_to_view : Axes ,
38
75
* args ,
39
- image_interpolation = "nearest" ,
76
+ image_interpolation : str = "nearest" ,
40
77
** kwargs
41
78
):
42
79
"""
@@ -70,90 +107,68 @@ def __init__(
70
107
ViewAxes
71
108
The new zoom view axes instance...
72
109
"""
73
- super ().__init__ (axes_to_view .figure , * args , zorder = zorder ,
74
- ** kwargs )
110
+ super ().__init__ (axes_to_view .figure , * args , ** kwargs )
75
111
self ._init_vars (axes_to_view , image_interpolation )
76
112
77
-
78
113
def _init_vars (
79
114
self ,
80
- axes_to_view ,
81
- image_interpolation = "nearest"
115
+ axes_to_view : Axes ,
116
+ image_interpolation : str = "nearest"
82
117
):
83
118
self .__view_axes = axes_to_view
84
119
self .__image_interpolation = image_interpolation
85
120
self ._render_depth = 0
86
121
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
89
158
if (self ._render_depth >= self .MAX_RENDER_DEPTH ):
90
159
return
91
160
self ._render_depth += 1
161
+ # Set the renderer, causing get_children to return the view's
162
+ # children also...
163
+ self .__renderer = renderer
92
164
93
165
super ().draw (renderer )
94
166
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
154
169
self ._render_depth -= 1
155
170
156
- def get_linescaling (self ):
171
+ def get_linescaling (self ) -> bool :
157
172
"""
158
173
Get if line width scaling is enabled.
159
174
@@ -164,7 +179,7 @@ def get_linescaling(self):
164
179
"""
165
180
return self .__scale_lines
166
181
167
- def set_linescaling (self , value ):
182
+ def set_linescaling (self , value : bool ):
168
183
"""
169
184
Set whether line widths should be scaled when rendering a view of
170
185
an axes.
@@ -178,7 +193,12 @@ def set_linescaling(self, value):
178
193
self .__scale_lines = value
179
194
180
195
@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
+ ):
182
202
axes .__class__ = cls
183
203
axes ._init_vars (axes_to_view , image_interpolation )
184
204
return axes
0 commit comments