Skip to content

ENH add an inset_axes to the axes class #11026

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

Merged
merged 1 commit into from
Jul 20, 2018
Merged
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
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ per-file-ignores =
examples/subplots_axes_and_figures/demo_constrained_layout.py: E402
examples/subplots_axes_and_figures/demo_tight_layout.py: E402
examples/subplots_axes_and_figures/two_scales.py: E402
examples/subplots_axes_and_figures/zoom_inset_axes.py: E402
examples/tests/backend_driver_sgskip.py: E402, E501
examples/text_labels_and_annotations/annotation_demo.py: E501
examples/text_labels_and_annotations/custom_legends.py: E402
Expand Down
3 changes: 3 additions & 0 deletions doc/api/axes_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ Text and Annotations
Axes.text
Axes.table
Axes.arrow
Axes.inset_axes
Axes.indicate_inset
Axes.indicate_inset_zoom


Fields
Expand Down
6 changes: 3 additions & 3 deletions examples/axes_grid1/inset_locator_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"""

###############################################################################
# The `.inset_locator`'s `~.inset_axes` allows to easily place insets in the
# corners of the axes by specifying a width and height and optionally
# a location (loc) which accepts locations as codes, similar to
# The `.inset_locator`'s `~.axes_grid1.inset_axes` allows to easily place
# insets in the corners of the axes by specifying a width and height and
# optionally a location (loc) which accepts locations as codes, similar to
# `~matplotlib.axes.Axes.legend`.
# By default, the inset is offset by some points from the axes - this is
# controlled via the `borderpad` parameter.
Expand Down
60 changes: 60 additions & 0 deletions examples/subplots_axes_and_figures/zoom_inset_axes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
======================
Zoom region inset axes
======================
Example of an inset axes and a rectangle showing where the zoom is located.
"""

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)
# z is a numpy array of 15x15
return z, (-3, 4, -4, 3)
Copy link
Member

Choose a reason for hiding this comment

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

Just stumbled over this. To make it clear what those magic numbers are use

extent = (-3, 4, -4, 3)
return z, extent

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, fixed, though I have a sneaky suspicion that I had it that way, and someone suggested I shorten it (ahem, @anntzer 😉)

Copy link
Contributor

Choose a reason for hiding this comment

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

For once I don't think it's my fault :) (?)


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

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

# inset axes....
axins = ax.inset_axes([0.5, 0.5, 0.47, 0.47])
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)

plt.show()

#############################################################################
#
# ------------
#
# References
# """"""""""
#
# The use of the following functions and methods is shown in this example:

import matplotlib
matplotlib.axes.Axes.inset_axes
matplotlib.axes.Axes.indicate_inset_zoom
matplotlib.axes.Axes.imshow
249 changes: 249 additions & 0 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,37 @@ def _plot_args_replacer(args, data):
"multiple plotting calls instead.")


def _make_inset_locator(bounds, trans, 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*.
"""
_bounds = mtransforms.Bbox.from_bounds(*bounds)
_trans = trans
_parent = parent

def inset_locator(ax, renderer):
bbox = _bounds
bb = mtransforms.TransformedBbox(bbox, _trans)
tr = _parent.figure.transFigure.inverted()
bb = mtransforms.TransformedBbox(bb, tr)
return bb

return inset_locator


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


class Axes(_AxesBase):
"""
The :class:`Axes` contains most of the figure elements:
Expand Down Expand Up @@ -390,6 +418,227 @@ def legend(self, *args, **kwargs):
def _remove_legend(self, legend):
self.legend_ = None

def inset_axes(self, bounds, *, transform=None, zorder=5,
**kwargs):
"""
Add a child inset axes to this existing axes.
Warnings
--------
This method is experimental as of 3.0, and the API may change.
Parameters
----------
bounds : [x0, y0, width, height]
Lower-left corner of inset axes, and its width and height.
transform : `.Transform`
Defaults to `ax.transAxes`, i.e. the units of *rect* are in
axes-relative coordinates.
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
parent axes.
**kwargs
Other *kwargs* are passed on to the `axes.Axes` child axes.
Returns
-------
Axes
The created `.axes.Axes` instance.
Examples
--------
This example makes two inset axes, the first is in axes-relative
coordinates, and the second in data-coordinates::
fig, ax = plt.suplots()
ax.plot(range(10))
axin1 = ax.inset_axes([0.8, 0.1, 0.15, 0.15])
axin2 = ax.inset_axes(
[5, 7, 2.3, 2.3], transform=ax.transData)
"""
if transform is None:
transform = self.transAxes
label = kwargs.pop('label', 'inset_axes')
Copy link
Contributor

Choose a reason for hiding this comment

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

would tend to make this and zorder keyword-only args (see my kwonly-args PR for example). again, not a big deal.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, I see why I ignored you here - this is pretty clear kwarg passing down to axes. I don't want to pull individual kwargs out for documentation reasons that are just passed down, but on the other hand I do want to set the default to something different than what Axes would set it to by itself. Moreover, 99% of the time, no one wants to define the label...


# This puts the rectangle into figure-relative coordinates.
inset_locator = _make_inset_locator(bounds, transform, self)
bb = inset_locator(None, None)

inset_ax = Axes(self.figure, bb.bounds, zorder=zorder,
label=label, **kwargs)

# this locator lets the axes move if in data coordinates.
# it gets called in `ax.apply_aspect() (of all places)
inset_ax.set_axes_locator(inset_locator)

self.add_child_axes(inset_ax)

return inset_ax

def indicate_inset(self, bounds, inset_ax=None, *, transform=None,
facecolor='none', edgecolor='0.5', alpha=0.5,
zorder=4.99, **kwargs):
"""
Add an inset indicator to the axes. This is a rectangle on the plot
at the position indicated by *bounds* that optionally has lines that
connect the rectangle to an inset axes
(`.Axes.inset_axes`).
Warnings
--------
This method is experimental as of 3.0, and the API may change.
Parameters
----------
bounds : [x0, y0, width, height]
Lower-left corner of rectangle to be marked, and its width
and height.
inset_ax : `.Axes`
An optional inset axes to draw connecting lines to. Two lines are
drawn connecting the indicator box to the inset axes on corners
chosen so as to not overlap with the indicator box.
transform : `.Transform`
Transform for the rectangle co-ordinates. Defaults to
`ax.transAxes`, i.e. the units of *rect* are in axes-relative
coordinates.
facecolor : Matplotlib color
Facecolor of the rectangle (default 'none').
edgecolor : Matplotlib color
Color of the rectangle and color of the connecting lines. Default
is '0.5'.
alpha : number
Transparency of the rectangle and connector lines. Default is 0.5.
zorder : number
Drawing order of the rectangle and connector lines. Default is 4.99
(just below the default level of inset axes).
**kwargs
Other *kwargs* are passed on to the rectangle patch.
Returns
-------
rectangle_patch: `.Patches.Rectangle`
Rectangle artist.
connector_lines: 4-tuple of `.Patches.ConnectionPatch`
One for each of four connector lines. Two are set with visibility
to *False*, but the user can set the visibility to True if the
automatic choice is not deemed correct.
"""

# to make the axes connectors work, we need to apply the aspect to
# the parent axes.
self.apply_aspect()

if transform is None:
transform = self.transData
label = kwargs.pop('label', 'indicate_inset')

xy = (bounds[0], bounds[1])
rectpatch = mpatches.Rectangle(xy, bounds[2], bounds[3],
facecolor=facecolor, edgecolor=edgecolor, alpha=alpha,
zorder=zorder, label=label, transform=transform, **kwargs)
self.add_patch(rectpatch)

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

pos = inset_ax.get_position() # this is in fig-fraction.
coordsA = 'axes fraction'
connects = []
xr = [bounds[0], bounds[0]+bounds[2]]
yr = [bounds[1], bounds[1]+bounds[3]]
for xc in range(2):
for yc in range(2):
xyA = (xc, yc)
xyB = (xr[xc], yr[yc])
connects += [mpatches.ConnectionPatch(xyA, xyB,
'axes fraction', 'data',
axesA=inset_ax, axesB=self, arrowstyle="-",
zorder=zorder, edgecolor=edgecolor, alpha=alpha)]
self.add_patch(connects[-1])
# decide which two of the lines to keep visible....
pos = inset_ax.get_position()
bboxins = pos.transformed(self.figure.transFigure)
rectbbox = mtransforms.Bbox.from_bounds(
*bounds).transformed(transform)
if rectbbox.x0 < bboxins.x0:
sig = 1
else:
sig = -1
if sig*rectbbox.y0 < sig*bboxins.y0:
connects[0].set_visible(False)
connects[3].set_visible(False)
else:
connects[1].set_visible(False)
connects[2].set_visible(False)

return rectpatch, connects

def indicate_inset_zoom(self, inset_ax, **kwargs):
"""
Add an inset indicator rectangle to the axes based on the axis
limits for an *inset_ax* and draw connectors between *inset_ax*
and the rectangle.
Warnings
--------
This method is experimental as of 3.0, and the API may change.
Parameters
----------
inset_ax : `.Axes`
Inset axes to draw connecting lines to. Two lines are
drawn connecting the indicator box to the inset axes on corners
chosen so as to not overlap with the indicator box.
**kwargs
Other *kwargs* are passed on to `.Axes.inset_rectangle`
Returns
-------
rectangle_patch: `.Patches.Rectangle`
Rectangle artist.
connector_lines: 4-tuple of `.Patches.ConnectionPatch`
One for each of four connector lines. Two are set with visibility
to *False*, but the user can set the visibility to True if the
automatic choice is not deemed correct.
"""

xlim = inset_ax.get_xlim()
ylim = inset_ax.get_ylim()
rect = [xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]]
rectpatch, connects = self.indicate_inset(
rect, inset_ax, **kwargs)

return rectpatch, connects

def text(self, x, y, s, fontdict=None, withdash=False, **kwargs):
"""
Add text to the axes.
Expand Down
Loading