Thanks to visit codestin.com
Credit goes to github.com

Skip to content

ENH: inset_axes anchored-artist API #11730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 56 additions & 6 deletions examples/subplots_axes_and_figures/zoom_inset_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@ def get_demo_image():
import numpy as np
f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False)
z = np.load(f)
Z2 = np.zeros([150, 150], dtype="d")
ny, nx = z.shape
Z2[30:30 + ny, 30:30 + nx] = z

# z is a numpy array of 15x15
return z, (-3, 4, -4, 3)
extent = (-3, 4, -4, 3)
return Z2, extent

fig, ax = plt.subplots(figsize=[5, 4])

# make data
Z, extent = get_demo_image()
Z2 = np.zeros([150, 150], dtype="d")
ny, nx = Z.shape
Z2[30:30 + ny, 30:30 + nx] = Z

Z2, extent = get_demo_image()
ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")

Expand All @@ -42,7 +43,56 @@ def get_demo_image():
axins.set_yticklabels('')

ax.indicate_inset_zoom(axins)
fig.canvas.draw()
plt.show()

#############################################################################
# There is a second interface that closely parallels the interface for
# `~.axes.legend` whereby we specify a location for the inset axes using
# a string code.

fig, ax = plt.subplots(figsize=[5, 4])

ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")

# inset axes....
axins = ax.inset_axes('NE', width=0.5, height=0.5)

axins.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
# sub region of the original image
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
axins.set_xticklabels('')
axins.set_yticklabels('')

ax.indicate_inset_zoom(axins)
fig.canvas.draw()
plt.show()

#############################################################################
# Its possible to use either form with a transform in data space instead of
# in the axes-relative co-ordinates:

fig, ax = plt.subplots(figsize=[5, 4])

ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")

# inset axes....
axins = ax.inset_axes([-2.5, 0, 1.6, 1.6], transform=ax.transData)

axins.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
# sub region of the original image
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
axins.set_xticklabels('')
axins.set_yticklabels('')

ax.indicate_inset_zoom(axins)
fig.canvas.draw()
plt.show()

#############################################################################
Expand Down
120 changes: 112 additions & 8 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def _make_inset_locator(bounds, trans, parent):
`.Axes.inset_axes`.

A locator gets used in `Axes.set_aspect` to override the default
locations... It is a function that takes an axes object and
locations. It is a function that takes an axes object and
a renderer and tells `set_aspect` where it is to be placed.

Here *rect* is a rectangle [l, b, w, h] that specifies the
Expand All @@ -111,6 +111,80 @@ def inset_locator(ax, renderer):
return inset_locator


def _make_inset_locator_anchored(loc, borderpad, width, height,
bbox_to_anchor, transform, parent):
"""
Helper function to locate inset axes, used in
`.Axes.inset_axes`.

A locator gets used in `Axes.set_aspect` to override the default
locations. It is a function that takes an axes object and
a renderer and tells `set_aspect` where it is to be placed.

Here *rect* is a rectangle [l, b, w, h] that specifies the
location for the axes in the transform given by *trans* on the
*parent*.
"""
codes = {'upper right': 'NE',
'upper left': 'NW',
'lower left': 'SW',
'lower right': 'SE',
'right': 'E',
'left': 'W',
'bottom': 'S',
'top': 'N',
'center left': 'W',
'center right': 'E',
'lower center': 'S',
'upper center': 'N',
'center': 'C'
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the center itself? I.e. loc code 10.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks for all the help!


if loc in codes:
loc = codes[loc]
if loc not in ['N', 'S', 'E', 'W', 'NE', 'NW', 'SE', 'SW', 'C']:
warnings.warn('inset_axes location "{}" not recognized; '
'Set to "NE".'.format(loc))
loc = 'NE'
_loc = loc
_parent = parent
_borderpadder = borderpad
if _borderpadder is None:
_borderpadder = _parent.xaxis.get_ticklabels()[0].get_size() * 0.5

_basebox = mtransforms.Bbox.from_bounds(0, 0, width, height)

_transform = transform
if _transform is None:
_transform = mtransforms.BboxTransformTo(_parent.bbox)

_bbox_to_anchor = bbox_to_anchor
if not isinstance(_bbox_to_anchor, mtransforms.BboxBase):
try:
l = len(_bbox_to_anchor)
except TypeError:
raise ValueError("Invalid argument for bbox_to_anchor : %s" %
str(_bbox_to_anchor))
if l == 2:
_bbox_to_anchor = [_bbox_to_anchor[0], _bbox_to_anchor[1],
width, height]
_bbox_to_anchor = mtransforms.Bbox.from_bounds(*_bbox_to_anchor)
_bbox_to_anchor = mtransforms.TransformedBbox(_bbox_to_anchor,
_transform)

def inset_locator(ax, renderer):
bbox = mtransforms.TransformedBbox(_basebox, _transform)
borderpad = renderer.points_to_pixels(_borderpadder)
anchored_box = bbox.anchored(loc,
container=_bbox_to_anchor.padded(-borderpad))
tr = _parent.figure.transFigure.inverted()
anchored_box = mtransforms.TransformedBbox(anchored_box, tr)

return anchored_box

return inset_locator


# The axes module contains all the wrappers to plotting functions.
# All the other methods should go in the _AxesBase class.

Expand Down Expand Up @@ -419,7 +493,8 @@ def _remove_legend(self, legend):
self.legend_ = None

def inset_axes(self, bounds, *, transform=None, zorder=5,
**kwargs):
borderaxespad=None, width=None, height=None,
bbox_to_anchor=None, **kwargs):
"""
Add a child inset axes to this existing axes.

Expand All @@ -431,13 +506,28 @@ def inset_axes(self, bounds, *, transform=None, zorder=5,
Parameters
----------

bounds : [x0, y0, width, height]
Lower-left corner of inset axes, and its width and height.
bounds : [x0, y0, width, height] or string.
If four-tupple: lower-left corner of inset axes, and its width and
height.

If a string, then locations such as "NE", "N", "NW", "W", etc,
or "upper right", "top", "upper left", etc (see `~.axes.legend`)
for codes. (Note we do *not* support the numerical codes).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as other functions like legend support the number codes, I would support them everywhere. Once it is decided to deprectate them, I would deprecate them all, everywhere, in one single rush. One might silently allow them here, without mentionning them specifically though. <- my personal opinion (I'm not too inclined to open that discussion again as it will just never get to an end.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hear you, but someone can add that as a followup PR if they really feel strongly about it. Its a bunch of extra code for something we don't really want to support any longer.


transform : `.Transform`
Defaults to `ax.transAxes`, i.e. the units of *rect* are in
axes-relative coordinates.

width, height : number
width and height of the inset axes. Only used if ``bounds`` is
a string. Units are set by ``transform``, and default to
axes-relative co-ordinates.

borderaxespad : number
If ``bounds`` is a string, this is the padding between the inset
axes and the parent axes in points. Defaults to half the fontsize
of the tick labels.

zorder : number
Defaults to 5 (same as `.Axes.legend`). Adjust higher or lower
to change whether it is above or below data plotted on the
Expand Down Expand Up @@ -470,9 +560,22 @@ def inset_axes(self, bounds, *, transform=None, zorder=5,
transform = self.transAxes
label = kwargs.pop('label', 'inset_axes')

# This puts the rectangle into figure-relative coordinates.
inset_locator = _make_inset_locator(bounds, transform, self)
bb = inset_locator(None, None)
if isinstance(bounds, str):
# i.e. NE, S, etc
if width is None:
width = 0.25
if height is None:
height = 0.25
if bbox_to_anchor is None:
bbox_to_anchor = self.bbox
inset_locator = _make_inset_locator_anchored(bounds,
borderaxespad, width, height, bbox_to_anchor,
transform, self)
else:
# This puts the rectangle into figure-relative coordinates.
inset_locator = _make_inset_locator(bounds, transform, self)

bb = inset_locator(None, self.figure.canvas.get_renderer())

inset_ax = Axes(self.figure, bb.bounds, zorder=zorder,
label=label, **kwargs)
Expand Down Expand Up @@ -549,6 +652,7 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None,

# to make the axes connectors work, we need to apply the aspect to
# the parent axes.

self.apply_aspect()

if transform is None:
Expand All @@ -563,7 +667,7 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None,

if inset_ax is not None:
# want to connect the indicator to the rect....

inset_ax.apply_aspect()
pos = inset_ax.get_position() # this is in fig-fraction.
coordsA = 'axes fraction'
connects = []
Expand Down
48 changes: 48 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5782,3 +5782,51 @@ def test_zoom_inset():
[0.8425, 0.907692]])
np.testing.assert_allclose(axin1.get_position().get_points(),
xx, rtol=1e-4)


def test_inset_codes():
"""
Test that the inset codes put the inset where we want...
"""

fig, ax = plt.subplots()
poss = [[[0.415625, 0.686111],
[0.609375, 0.886111]],
[[0.695833, 0.686111],
[0.889583, 0.886111]],
[[0.695833, 0.4],
[0.889583, 0.6]],
[[0.695833, 0.113889],
[0.889583, 0.313889]],
[[0.415625, 0.113889],
[0.609375, 0.313889]],
[[0.135417, 0.113889],
[0.329167, 0.313889]],
[[0.135417, 0.4],
[0.329167, 0.6]],
[[0.135417, 0.686111],
[0.329167, 0.886111]],
[[0.415625, 0.4],
[0.609375, 0.6]]]
codes = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'C']
for pos, code in zip(poss, codes):
axin1 = ax.inset_axes(code)
np.testing.assert_allclose(axin1.get_position().get_points(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to define some bboxes in axes coordinates here and compare to their coordinates instead? (Seems like this test is pretty susceptible to unrelated changes in the subplot positioning.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string-code positioning has a borderpad that is in pixels, not axes co-ordinates, so knowing the bbox a-priori would just mean I was duplicating the code in the method. I guess that'd test if someone changed it ...

pos, rtol=1e-4)
del axin1

# test synonyms
syns = ['top', 'upper right', 'center right', 'lower right', 'bottom',
'lower left', 'center left', 'upper left', 'center']
codes = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'C']
for syn, code in zip(syns, codes):
axin1 = ax.inset_axes(code)
axin2 = ax.inset_axes(syn)
np.testing.assert_allclose(axin1.get_position().get_points(),
axin2.get_position().get_points(), rtol=1e-4)

# test the borderaxespad
axin1 = ax.inset_axes('NE', borderaxespad=20)
pos = [[0.671528, 0.653704], [0.865278, 0.853704]]
np.testing.assert_allclose(axin1.get_position().get_points(),
pos, rtol=1e-4)