-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
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 |
---|---|---|
@@ -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) | ||
|
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
@@ -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') | ||
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 tend to make this and zorder keyword-only args (see my kwonly-args PR for example). again, not a big deal. 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. 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. | ||
|
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.
Just stumbled over this. To make it clear what those magic numbers are use
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.
Thanks, fixed, though I have a sneaky suspicion that I had it that way, and someone suggested I shorten it (ahem, @anntzer 😉)
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.
For once I don't think it's my fault :) (?)