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

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Jul 21, 2018

PR Summary

ax.inset_axes is now merged for 3.0 (#11026) with a minimal positioning API. (ax.inset_axes([l, b, w, h])), but folks can specify transforms that allow a pad with the edge of the axes.

This PR expands the API to be similar to ax.legend and axes_grid1.inset_axes and allow a string for the position argument. So now we can do ax.inset_axes('NE', width=0.4, height=0.4)

import matplotlib.pyplot as plt
import numpy as np


def get_demo_image():
    from matplotlib.cbook import get_sample_data
    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
    extent = (-3, 4, -4, 3)
    return Z2, extent

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

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

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

axins.imshow(Z2, extent=extent, interpolation="nearest",
          origin="lower")
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
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()

boo

PR Checklist

  • Has Pytest style unit tests
  • Code is PEP 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@jklymak jklymak force-pushed the enh-inset-axes-anchored branch from bba682b to 5207b60 Compare July 21, 2018 23:08
@jklymak
Copy link
Member Author

jklymak commented Jul 21, 2018

ping @ImportanceOfBeingErnest who's concern about the API sparked this, though note the API is not exactly the same...

@jklymak jklymak added this to the v3.1 milestone Jul 21, 2018
@jklymak jklymak force-pushed the enh-inset-axes-anchored branch from 5207b60 to 4275e09 Compare July 21, 2018 23:17
@ImportanceOfBeingErnest
Copy link
Member

Yeah, I guess the options are discussed in #11026 (comment), so if the aim is to truely replace axes_grid1.inset_locator, something like the mentionned add_inset_complete would be needed. But doing this step by step is of course an option and this PR then seems to do the second step.

`.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

Choose a reason for hiding this comment

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

remove ellipsis (...)

height.

If a string, then locations such as "NE", "N", "NW", "W", etc,
of "upper right", "top", "upper left", etc (see `~.axes.legend`)

Choose a reason for hiding this comment

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

or "upper right"

Choose a reason for hiding this comment

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

And if "center" is unsopported (see above), one cannot simply link to axes.legend.


If a string, then locations such as "NE", "N", "NW", "W", etc,
of "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.

@@ -48,10 +48,10 @@ def paintEvent(self, event):
[[left, self.renderer.height - (top + height * self._dpi_ratio)],
[left + width * self._dpi_ratio, self.renderer.height - top]])
reg = self.copy_from_bbox(bbox)
buf = reg.to_string_argb()
# buf = reg.to_string_argb()
buf = memoryview(reg)

Choose a reason for hiding this comment

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

I'm almost certain that this should not be part of this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ha, no...



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

Choose a reason for hiding this comment

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

Single double-quotes as function docs are pretty non-standard. Not sure if it matters for tests though.

codes = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
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 ...

'center right': 'E',
'lower center': 'S',
'upper center': 'N',
}

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!

@jklymak
Copy link
Member Author

jklymak commented Jul 21, 2018

something like the mentionned add_inset_complete would be needed.

I think this has all the features of add_inset_complete. Note the transform kwarg works with the new API and the borderaxespad parameter gives the padding. You can also specify a bbox_to_anchor. The difference is that we need to specify width and height or take the defaults, which you don't need to do for legend because it shrinks to fit.

@jklymak jklymak force-pushed the enh-inset-axes-anchored branch 2 times, most recently from c43c4b5 to f41aa99 Compare July 22, 2018 00:05
@jklymak jklymak force-pushed the enh-inset-axes-anchored branch from f41aa99 to d61a940 Compare July 22, 2018 02:33
@ImportanceOfBeingErnest
Copy link
Member

I went through the inset_locator_demo and indeed almost all cases are feasible with the proposed API.

Here is the changed example code
import matplotlib.pyplot as plt
#from mpl_toolkits.axes_grid1.inset_locator import inset_axes
#
fig, (ax, ax2) = plt.subplots(1, 2, figsize=[5.5, 2.8])

# Create inset of width 1.3 inches and height 0.9 inches
# at the default upper right location
#axins = inset_axes(ax, width=1.3, height=0.9)
axins = ax.inset_axes("NE", width=1.3, height=0.9, transform=fig.dpi_scale_trans)

# Create inset of width 30% and height 40% of the parent axes' bounding box
# at the lower left corner (loc=3)
#axins2 = inset_axes(ax, width="30%", height="40%", loc=3)
axins2 = ax.inset_axes("SW", width=0.3, height=.4)

# Create inset of mixed specifications in the second subplot;
# width is 30% of parent axes' bounding box and
# height is 1 inch at the upper left corner (loc=2)
#axins3 = inset_axes(ax2, width="30%", height=1., loc=2)
from matplotlib.transforms import blended_transform_factory
trans = blended_transform_factory(ax2.transAxes, fig.dpi_scale_trans)
axins3 = ax2.inset_axes("NW", width=0.3, height=1., transform=trans)

# Create an inset in the lower right corner (loc=4) with borderpad=1, i.e.
# 10 points padding (as 10pt is the default fontsize) to the parent axes
#axins4 = inset_axes(ax2, width="20%", height="20%", loc=4, borderpad=1)
axins4 = ax2.inset_axes("SE", width=.2, height=.2, borderaxespad=10)

# Turn ticklabels of insets off
for axi in [axins, axins2, axins3, axins4]:
    axi.tick_params(labelleft=False, labelbottom=False)

plt.show()

## =============================================================================
##
from matplotlib import transforms as mtrans
fig = plt.figure(figsize=[5.5, 2.8])
ax = fig.add_subplot(121)

# We use the axes transform as bbox_transform. Therefore the bounding box
# needs to be specified in axes coordinates ((0,0) is the lower left corner
# of the axes, (1,1) is the upper right corner).
# The bounding box (.2, .4, .6, .5) starts at (.2,.4) and ranges to (.8,.9)
# in those coordinates.
# Inside of this bounding box an inset of half the bounding box' width and
# three quarters of the bounding box' height is created. The lower left corner
# of the inset is aligned to the lower left corner of the bounding box (loc=3).
# The inset is then offset by the default 0.5 in units of the font size.

#axins = inset_axes(ax, width="50%", height="75%",
#                   bbox_to_anchor=(.2, .4, .6, .5),
#                   bbox_transform=ax.transAxes, loc=3)
# This is hard to reproduce, because padding is applied towards the axes
# There is no bounding box.
trans = ax.transAxes  + mtrans.ScaledTranslation(+5/72., +5/72, fig.dpi_scale_trans) 
axins = ax.inset_axes((.2, .4, .6*0.5, .5*0.75), transform=trans)


# For visualization purposes we mark the bounding box by a rectangle
ax.add_patch(plt.Rectangle((.2, .4), .6, .5, ls="--", ec="c", fc="None",
                           transform=ax.transAxes))

# We set the axis limits to something other than the default, in order to not
# distract from the fact that axes coodinates are used here.
ax.axis([0, 10, 0, 10])


# Note how the two following insets are created at the same positions, one by
# use of the default parent axes' bbox and the other via a bbox in axes
# coordinates and the respective transform.
ax2 = fig.add_subplot(222)
#axins2 = inset_axes(ax2, width="30%", height="50%")
axins2 = ax2.inset_axes("NE", width=.3, height=.5)


ax3 = fig.add_subplot(224)
trans = ax3.transAxes  + mtrans.ScaledTranslation(-5/72., -5/72, fig.dpi_scale_trans) 
axins3 = ax3.inset_axes((.7, .5, .3, .5), width="100%", height="100%",
                        transform=trans)

# For visualization purposes we mark the bounding box by a rectangle
ax2.add_patch(plt.Rectangle((0, 0), 1, 1, ls="--", lw=2, ec="c", fc="None"))
ax3.add_patch(plt.Rectangle((.7, .5), .3, .5, ls="--", lw=2,
                            ec="c", fc="None"))

# Turn ticklabels off
for axi in [axins2, axins3, ax2, ax3]:
    axi.tick_params(labelleft=False, labelbottom=False)

plt.show()
#
#
## =============================================================================
##
#
#
fig = plt.figure(figsize=[5.5, 2.8])
ax = fig.add_subplot(131)

# Create an inset outside the axes
#axins = inset_axes(ax, width="100%", height="100%",
#                   bbox_to_anchor=(1.05, .6, .5, .4),
#                   bbox_transform=ax.transAxes, loc=2, borderpad=0)
axins = ax.inset_axes((1.05, .6, .5, .4))
axins.tick_params(left=False, right=True, labelleft=False, labelright=True)

# Create an inset with a 2-tuple bounding box. Note that this creates a
# bbox without extent. This hence only makes sense when specifying
# width and height in absolute units (inches).
#axins2 = inset_axes(ax, width=0.5, height=0.4,
#                    bbox_to_anchor=(0.33, 0.25),
#                    bbox_transform=ax.transAxes, loc=3, borderpad=0)
# -----------
#This seems impossible, even when using a 4 tuple bounds.
# -----------

ax2 = fig.add_subplot(133)
ax2.set_xscale("log")
ax2.axis([1e-6, 1e6, -2, 6])

# Create inset in data coordinates using ax.transData as transform
#axins3 = inset_axes(ax2, width="100%", height="100%",
#                    bbox_to_anchor=(1e-2, 2, 1e3, 3),
#                    bbox_transform=ax2.transData, loc=2, borderpad=0)
axins3 = ax2.inset_axes((1e-2, 2, 1e3, 3), transform=ax2.transData)


# Create an inset horizontally centered in figure coordinates and vertically
# bound to line up with the axes.
from matplotlib.transforms import blended_transform_factory
transform = blended_transform_factory(fig.transFigure, ax2.transAxes)
#axins4 = inset_axes(ax2, width="16%", height="34%",
#                    bbox_to_anchor=(0, 0, 1, 1),
#                    bbox_transform=transform, loc=8, borderpad=0)
axins4 = ax2.inset_axes((.5-.16/2, 0, .16, .34), transform=transform)
plt.show()

While some cases become much more straight forward, e.g.

axins = inset_axes(ax, width="100%", height="100%",
                   bbox_to_anchor=(1.05, .6, .5, .4),
                   bbox_transform=ax.transAxes, loc=2, borderpad=0)

becomes

axins = ax.inset_axes((1.05, .6, .5, .4))

others are more intricate, e.g.

axins3 = inset_axes(ax2, width="30%", height=1., loc=2)

becomes:

from matplotlib.transforms import blended_transform_factory
trans = blended_transform_factory(ax2.transAxes, fig.dpi_scale_trans)
axins3 = ax2.inset_axes("NW", width=0.3, height=1., transform=trans)

Now there is one example, where I did not find any way to reproduce. This should create an 0.5 by 0.4 inch inset at position (0.33, 0.25) in axes coordinates.

axins2 = inset_axes(ax, width=0.5, height=0.4,
                    bbox_to_anchor=(0.33, 0.25),
                    bbox_transform=ax.transAxes, loc=3, borderpad=0)

Despite this I would still approve this, as the API is in general much simplified in contrast to the axes_grid1 solution. Still, the above shows that the axes_grid1 method has a lot of benefits and is still more generally applicable.

Copy link
Member

@ImportanceOfBeingErnest ImportanceOfBeingErnest left a comment

Choose a reason for hiding this comment

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

Despite some minor drawbacks this solution greatly simplifies the usual inset creation process. See comment above for details.

@jklymak
Copy link
Member Author

jklymak commented Aug 7, 2018

@ImportanceOfBeingErnest thanks for looking at this so carefully.

There is no huge rush on this, so I will look at your cases and see if an API can be worked out that can cover those as well. Moving back to WIP...

@jklymak jklymak modified the milestones: v3.1.0, v3.2.0 Feb 5, 2019
@tacaswell tacaswell modified the milestones: v3.2.0, v3.3.0 Aug 25, 2019
@QuLogic QuLogic modified the milestones: v3.3.0, v3.4.0 Apr 30, 2020
@jklymak jklymak marked this pull request as draft July 23, 2020 16:37
@QuLogic QuLogic modified the milestones: v3.4.0, v3.5.0 Jan 21, 2021
@QuLogic QuLogic modified the milestones: v3.5.0, v3.6.0 Aug 21, 2021
@timhoffm timhoffm modified the milestones: v3.6.0, unassigned Apr 30, 2022
@story645 story645 modified the milestones: unassigned, needs sorting Oct 6, 2022
@jklymak jklymak closed this Feb 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants