-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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' | ||
} | ||
|
||
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. | ||
|
||
|
@@ -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. | ||
|
||
|
@@ -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). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As long as other functions like There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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) | ||
|
@@ -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: | ||
|
@@ -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 = [] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to define some There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
There was a problem hiding this comment.
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
.There was a problem hiding this comment.
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!