Skip to content

Commit 0d604bf

Browse files
committed
ENH: add an inset_axes to the axes class
1 parent 691fb7f commit 0d604bf

File tree

6 files changed

+370
-0
lines changed

6 files changed

+370
-0
lines changed

.flake8

+1
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ per-file-ignores =
252252
examples/subplots_axes_and_figures/demo_constrained_layout.py: E402
253253
examples/subplots_axes_and_figures/demo_tight_layout.py: E402
254254
examples/subplots_axes_and_figures/two_scales.py: E402
255+
examples/subplots_axes_and_figures/zoom_inset_axes.py: E402
255256
examples/tests/backend_driver_sgskip.py: E402, E501
256257
examples/text_labels_and_annotations/annotation_demo.py: E501
257258
examples/text_labels_and_annotations/custom_legends.py: E402

doc/api/axes_api.rst

+3
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ Text and Annotations
181181
Axes.text
182182
Axes.table
183183
Axes.arrow
184+
Axes.inset_axes
185+
Axes.indicate_inset
186+
Axes.indicate_inset_zoom
184187

185188

186189
Fields
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
======================
3+
Zoom region inset axes
4+
======================
5+
6+
Example of an inset axes and a rectangle showing where the zoom is located.
7+
8+
"""
9+
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
13+
14+
def get_demo_image():
15+
from matplotlib.cbook import get_sample_data
16+
import numpy as np
17+
f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False)
18+
z = np.load(f)
19+
# z is a numpy array of 15x15
20+
return z, (-3, 4, -4, 3)
21+
22+
fig, ax = plt.subplots(figsize=[5, 4])
23+
24+
# make data
25+
Z, extent = get_demo_image()
26+
Z2 = np.zeros([150, 150], dtype="d")
27+
ny, nx = Z.shape
28+
Z2[30:30 + ny, 30:30 + nx] = Z
29+
30+
ax.imshow(Z2, extent=extent, interpolation="nearest",
31+
origin="lower")
32+
33+
# inset axes....
34+
axins = ax.inset_axes([0.5, 0.5, 0.47, 0.47])
35+
axins.imshow(Z2, extent=extent, interpolation="nearest",
36+
origin="lower")
37+
# sub region of the original image
38+
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
39+
axins.set_xlim(x1, x2)
40+
axins.set_ylim(y1, y2)
41+
axins.set_xticklabels('')
42+
axins.set_yticklabels('')
43+
44+
ax.indicate_inset_zoom(axins)
45+
46+
plt.show()
47+
48+
#############################################################################
49+
#
50+
# ------------
51+
#
52+
# References
53+
# """"""""""
54+
#
55+
# The use of the following functions and methods is shown in this example:
56+
57+
import matplotlib
58+
matplotlib.axes.Axes.inset_axes
59+
matplotlib.axes.Axes.indicate_inset_zoom
60+
matplotlib.axes.Axes.imshow

lib/matplotlib/axes/_axes.py

+249
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,37 @@ def _plot_args_replacer(args, data):
8484
"multiple plotting calls instead.")
8585

8686

87+
def _make_inset_locator(bounds, trans, parent):
88+
"""
89+
Helper function to locate inset axes, used in
90+
`.Axes.inset_axes`.
91+
92+
A locator gets used in `Axes.set_aspect` to override the default
93+
locations... It is a function that takes an axes object and
94+
a renderer and tells `set_aspect` where it is to be placed.
95+
96+
Here *rect* is a rectangle [l, b, w, h] that specifies the
97+
location for the axes in the transform given by *trans* on the
98+
*parent*.
99+
"""
100+
_bounds = mtransforms.Bbox.from_bounds(*bounds)
101+
_trans = trans
102+
_parent = parent
103+
104+
def inset_locator(ax, renderer):
105+
bbox = _bounds
106+
bb = mtransforms.TransformedBbox(bbox, _trans)
107+
tr = _parent.figure.transFigure.inverted()
108+
bb = mtransforms.TransformedBbox(bb, tr)
109+
return bb
110+
111+
return inset_locator
112+
113+
87114
# The axes module contains all the wrappers to plotting functions.
88115
# All the other methods should go in the _AxesBase class.
89116

117+
90118
class Axes(_AxesBase):
91119
"""
92120
The :class:`Axes` contains most of the figure elements:
@@ -390,6 +418,227 @@ def legend(self, *args, **kwargs):
390418
def _remove_legend(self, legend):
391419
self.legend_ = None
392420

421+
def inset_axes(self, bounds, *, transform=None, zorder=5,
422+
**kwargs):
423+
"""
424+
Add a child inset axes to this existing axes.
425+
426+
Warnings
427+
--------
428+
429+
This method is experimental as of 3.0, and the API may change.
430+
431+
Parameters
432+
----------
433+
434+
bounds : [x0, y0, width, height]
435+
Lower-left corner of inset axes, and its width and height.
436+
437+
transform : `.Transform`
438+
Defaults to `ax.transAxes`, i.e. the units of *rect* are in
439+
axes-relative coordinates.
440+
441+
zorder : number
442+
Defaults to 5 (same as `.Axes.legend`). Adjust higher or lower
443+
to change whether it is above or below data plotted on the
444+
parent axes.
445+
446+
**kwargs
447+
448+
Other *kwargs* are passed on to the `axes.Axes` child axes.
449+
450+
Returns
451+
-------
452+
453+
Axes
454+
The created `.axes.Axes` instance.
455+
456+
Examples
457+
--------
458+
459+
This example makes two inset axes, the first is in axes-relative
460+
coordinates, and the second in data-coordinates::
461+
462+
fig, ax = plt.suplots()
463+
ax.plot(range(10))
464+
axin1 = ax.inset_axes([0.8, 0.1, 0.15, 0.15])
465+
axin2 = ax.inset_axes(
466+
[5, 7, 2.3, 2.3], transform=ax.transData)
467+
468+
"""
469+
if transform is None:
470+
transform = self.transAxes
471+
label = kwargs.pop('label', 'inset_axes')
472+
473+
# This puts the rectangle into figure-relative coordinates.
474+
inset_locator = _make_inset_locator(bounds, transform, self)
475+
bb = inset_locator(None, None)
476+
477+
inset_ax = Axes(self.figure, bb.bounds, zorder=zorder,
478+
label=label, **kwargs)
479+
480+
# this locator lets the axes move if in data coordinates.
481+
# it gets called in `ax.apply_aspect() (of all places)
482+
inset_ax.set_axes_locator(inset_locator)
483+
484+
self.add_child_axes(inset_ax)
485+
486+
return inset_ax
487+
488+
def indicate_inset(self, bounds, inset_ax=None, *, transform=None,
489+
facecolor='none', edgecolor='0.5', alpha=0.5,
490+
zorder=4.99, **kwargs):
491+
"""
492+
Add an inset indicator to the axes. This is a rectangle on the plot
493+
at the position indicated by *bounds* that optionally has lines that
494+
connect the rectangle to an inset axes
495+
(`.Axes.inset_axes`).
496+
497+
Warnings
498+
--------
499+
500+
This method is experimental as of 3.0, and the API may change.
501+
502+
503+
Parameters
504+
----------
505+
506+
bounds : [x0, y0, width, height]
507+
Lower-left corner of rectangle to be marked, and its width
508+
and height.
509+
510+
inset_ax : `.Axes`
511+
An optional inset axes to draw connecting lines to. Two lines are
512+
drawn connecting the indicator box to the inset axes on corners
513+
chosen so as to not overlap with the indicator box.
514+
515+
transform : `.Transform`
516+
Transform for the rectangle co-ordinates. Defaults to
517+
`ax.transAxes`, i.e. the units of *rect* are in axes-relative
518+
coordinates.
519+
520+
facecolor : Matplotlib color
521+
Facecolor of the rectangle (default 'none').
522+
523+
edgecolor : Matplotlib color
524+
Color of the rectangle and color of the connecting lines. Default
525+
is '0.5'.
526+
527+
alpha : number
528+
Transparency of the rectangle and connector lines. Default is 0.5.
529+
530+
zorder : number
531+
Drawing order of the rectangle and connector lines. Default is 4.99
532+
(just below the default level of inset axes).
533+
534+
**kwargs
535+
Other *kwargs* are passed on to the rectangle patch.
536+
537+
Returns
538+
-------
539+
540+
rectangle_patch: `.Patches.Rectangle`
541+
Rectangle artist.
542+
543+
connector_lines: 4-tuple of `.Patches.ConnectionPatch`
544+
One for each of four connector lines. Two are set with visibility
545+
to *False*, but the user can set the visibility to True if the
546+
automatic choice is not deemed correct.
547+
548+
"""
549+
550+
# to make the axes connectors work, we need to apply the aspect to
551+
# the parent axes.
552+
self.apply_aspect()
553+
554+
if transform is None:
555+
transform = self.transData
556+
label = kwargs.pop('label', 'indicate_inset')
557+
558+
xy = (bounds[0], bounds[1])
559+
rectpatch = mpatches.Rectangle(xy, bounds[2], bounds[3],
560+
facecolor=facecolor, edgecolor=edgecolor, alpha=alpha,
561+
zorder=zorder, label=label, transform=transform, **kwargs)
562+
self.add_patch(rectpatch)
563+
564+
if inset_ax is not None:
565+
# want to connect the indicator to the rect....
566+
567+
pos = inset_ax.get_position() # this is in fig-fraction.
568+
coordsA = 'axes fraction'
569+
connects = []
570+
xr = [bounds[0], bounds[0]+bounds[2]]
571+
yr = [bounds[1], bounds[1]+bounds[3]]
572+
for xc in range(2):
573+
for yc in range(2):
574+
xyA = (xc, yc)
575+
xyB = (xr[xc], yr[yc])
576+
connects += [mpatches.ConnectionPatch(xyA, xyB,
577+
'axes fraction', 'data',
578+
axesA=inset_ax, axesB=self, arrowstyle="-",
579+
zorder=zorder, edgecolor=edgecolor, alpha=alpha)]
580+
self.add_patch(connects[-1])
581+
# decide which two of the lines to keep visible....
582+
pos = inset_ax.get_position()
583+
bboxins = pos.transformed(self.figure.transFigure)
584+
rectbbox = mtransforms.Bbox.from_bounds(
585+
*bounds).transformed(transform)
586+
if rectbbox.x0 < bboxins.x0:
587+
sig = 1
588+
else:
589+
sig = -1
590+
if sig*rectbbox.y0 < sig*bboxins.y0:
591+
connects[0].set_visible(False)
592+
connects[3].set_visible(False)
593+
else:
594+
connects[1].set_visible(False)
595+
connects[2].set_visible(False)
596+
597+
return rectpatch, connects
598+
599+
def indicate_inset_zoom(self, inset_ax, **kwargs):
600+
"""
601+
Add an inset indicator rectangle to the axes based on the axis
602+
limits for an *inset_ax* and draw connectors between *inset_ax*
603+
and the rectangle.
604+
605+
Warnings
606+
--------
607+
608+
This method is experimental as of 3.0, and the API may change.
609+
610+
Parameters
611+
----------
612+
613+
inset_ax : `.Axes`
614+
Inset axes to draw connecting lines to. Two lines are
615+
drawn connecting the indicator box to the inset axes on corners
616+
chosen so as to not overlap with the indicator box.
617+
618+
**kwargs
619+
Other *kwargs* are passed on to `.Axes.inset_rectangle`
620+
621+
Returns
622+
-------
623+
624+
rectangle_patch: `.Patches.Rectangle`
625+
Rectangle artist.
626+
627+
connector_lines: 4-tuple of `.Patches.ConnectionPatch`
628+
One for each of four connector lines. Two are set with visibility
629+
to *False*, but the user can set the visibility to True if the
630+
automatic choice is not deemed correct.
631+
632+
"""
633+
634+
xlim = inset_ax.get_xlim()
635+
ylim = inset_ax.get_ylim()
636+
rect = [xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]]
637+
rectpatch, connects = self.indicate_inset(
638+
rect, inset_ax, **kwargs)
639+
640+
return rectpatch, connects
641+
393642
def text(self, x, y, s, fontdict=None, withdash=False, **kwargs):
394643
"""
395644
Add text to the axes.

lib/matplotlib/axes/_base.py

+25
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,7 @@ def cla(self):
10311031
self.artists = []
10321032
self.images = []
10331033
self._mouseover_set = _OrderedSet()
1034+
self.child_axes = []
10341035
self._current_image = None # strictly for pyplot via _sci, _gci
10351036
self.legend_ = None
10361037
self.collections = [] # collection.Collection instances
@@ -1807,6 +1808,27 @@ def add_artist(self, a):
18071808
self.stale = True
18081809
return a
18091810

1811+
def add_child_axes(self, ax):
1812+
"""
1813+
Add a :class:`~matplotlib.axes.Axesbase` instance
1814+
as a child to the axes.
1815+
1816+
Returns the added axes.
1817+
1818+
This is the lowlevel version. See `.axes.Axes.inset_axes`
1819+
"""
1820+
1821+
# normally axes have themselves as the axes, but these need to have
1822+
# their parent...
1823+
# Need to bypass the getter...
1824+
ax._axes = self
1825+
ax.stale_callback = martist._stale_axes_callback
1826+
1827+
self.child_axes.append(ax)
1828+
ax._remove_method = self.child_axes.remove
1829+
self.stale = True
1830+
return ax
1831+
18101832
def add_collection(self, collection, autolim=True):
18111833
"""
18121834
Add a :class:`~matplotlib.collections.Collection` instance
@@ -4073,9 +4095,12 @@ def get_children(self):
40734095
children.append(self._right_title)
40744096
children.extend(self.tables)
40754097
children.extend(self.images)
4098+
children.extend(self.child_axes)
4099+
40764100
if self.legend_ is not None:
40774101
children.append(self.legend_)
40784102
children.append(self.patch)
4103+
40794104
return children
40804105

40814106
def contains(self, mouseevent):

0 commit comments

Comments
 (0)