Skip to content

Commit 416c238

Browse files
Add support for plotting items in a view
without them being hidden.
1 parent 0f7efe8 commit 416c238

File tree

3 files changed

+136
-76
lines changed

3 files changed

+136
-76
lines changed

matplotview/_view_axes.py

Lines changed: 95 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,47 @@
1+
import itertools
2+
from typing import Type, List
13
from matplotlib.axes import Axes
24
from matplotlib.transforms import Bbox
35
import matplotlib.docstring as docstring
46
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]:
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

matplotview/tests/test_inset_zoom.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from matplotlib.testing.decorators import check_figures_equal
44
from matplotview import view, inset_zoom_axes
55

6-
@check_figures_equal(tol=3)
6+
@check_figures_equal(tol=6)
77
def test_double_plot(fig_test, fig_ref):
88
np.random.seed(1)
99
im_data = np.random.rand(30, 30)
@@ -13,6 +13,7 @@ def test_double_plot(fig_test, fig_ref):
1313

1414
ax_test1.plot([i for i in range(10)], "r")
1515
ax_test1.add_patch(plt.Circle((3, 3), 1, ec="black", fc="blue"))
16+
ax_test1.text(10, 10, "Hello World!", size=14)
1617
ax_test1.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5,
1718
interpolation="nearest")
1819
ax_test2 = view(ax_test2, ax_test1)
@@ -25,10 +26,12 @@ def test_double_plot(fig_test, fig_ref):
2526

2627
ax_ref1.plot([i for i in range(10)], "r")
2728
ax_ref1.add_patch(plt.Circle((3, 3), 1, ec="black", fc="blue"))
29+
ax_ref1.text(10, 10, "Hello World!", size=14)
2830
ax_ref1.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5,
2931
interpolation="nearest")
3032
ax_ref2.plot([i for i in range(10)], "r")
3133
ax_ref2.add_patch(plt.Circle((3, 3), 1, ec="black", fc="blue"))
34+
ax_ref2.text(10, 10, "Hello World!", size=14)
3235
ax_ref2.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5,
3336
interpolation="nearest")
3437

@@ -65,4 +68,40 @@ def test_auto_zoom_inset(fig_test, fig_ref):
6568
axins_ref.add_patch(plt.Circle((3, 3), 1, ec="black", fc="blue"))
6669
axins_ref.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5,
6770
interpolation="nearest")
71+
ax_ref.indicate_inset_zoom(axins_ref, edgecolor="black")
72+
73+
74+
@check_figures_equal(tol=3.5)
75+
def test_plotting_in_view(fig_test, fig_ref):
76+
np.random.seed(1)
77+
im_data = np.random.rand(30, 30)
78+
arrow_s = dict(arrowstyle="->")
79+
80+
# Test Case...
81+
ax_test = fig_test.gca()
82+
ax_test.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5,
83+
interpolation="nearest")
84+
axins_test = inset_zoom_axes(ax_test, [0.5, 0.5, 0.48, 0.48])
85+
axins_test.set_linescaling(False)
86+
axins_test.set_xlim(1, 5)
87+
axins_test.set_ylim(1, 5)
88+
axins_test.annotate(
89+
"Interesting", (3, 3), (0, 0),
90+
textcoords="axes fraction", arrowprops=arrow_s
91+
)
92+
ax_test.indicate_inset_zoom(axins_test, edgecolor="black")
93+
94+
# Reference
95+
ax_ref = fig_ref.gca()
96+
ax_ref.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5,
97+
interpolation="nearest")
98+
axins_ref = ax_ref.inset_axes([0.5, 0.5, 0.48, 0.48])
99+
axins_ref.set_xlim(1, 5)
100+
axins_ref.set_ylim(1, 5)
101+
axins_ref.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5,
102+
interpolation="nearest")
103+
axins_ref.annotate(
104+
"Interesting", (3, 3), (0, 0),
105+
textcoords="axes fraction", arrowprops=arrow_s
106+
)
68107
ax_ref.indicate_inset_zoom(axins_ref, edgecolor="black")

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
classifiers=[
2121
'Development Status :: 3 - Alpha',
22+
'Framework :: Matplotlib',
2223
'License :: OSI Approved :: Python Software Foundation License',
2324
'Programming Language :: Python',
2425
'Programming Language :: Python :: 3',

0 commit comments

Comments
 (0)