From 60e9ec01c86b3060f6bffd4802502d31029d9b9b Mon Sep 17 00:00:00 2001 From: ImportanceOfBeingErnest Date: Thu, 12 Apr 2018 01:32:19 +0200 Subject: [PATCH] Fix inset_axes + doc --- examples/axes_grid1/inset_locator_demo.py | 152 +++++++++++++++---- examples/axes_grid1/inset_locator_demo2.py | 64 ++++++-- lib/mpl_toolkits/axes_grid1/inset_locator.py | 90 +++++++++-- lib/mpl_toolkits/tests/test_axes_grid1.py | 63 ++++++-- 4 files changed, 294 insertions(+), 75 deletions(-) diff --git a/examples/axes_grid1/inset_locator_demo.py b/examples/axes_grid1/inset_locator_demo.py index 0dfd611ef0d2..878d3a5b1b04 100644 --- a/examples/axes_grid1/inset_locator_demo.py +++ b/examples/axes_grid1/inset_locator_demo.py @@ -4,49 +4,141 @@ ================== """ + +############################################################################### +# 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 +# `~matplotlib.axes.Axes.legend`. +# By default, the inset is offset by some points from the axes - this is +# controlled via the `borderpad` parameter. + 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) + +# 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) + +# 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) + +# 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) + +# Turn ticklabels of insets off +for axi in [axins, axins2, axins3, axins4]: + axi.tick_params(labelleft=False, labelbottom=False) + +plt.show() -from mpl_toolkits.axes_grid1.inset_locator import inset_axes, zoomed_inset_axes -from mpl_toolkits.axes_grid1.anchored_artists import AnchoredSizeBar +############################################################################### +# The arguments `bbox_to_anchor` and `bbox_transfrom` can be used for a more +# fine grained control over the inset position and size or even to position +# the inset at completely arbitrary positions. +# The `bbox_to_anchor` sets the bounding box in coordinates according to the +# `bbox_transform`. +# -def add_sizebar(ax, size): - asb = AnchoredSizeBar(ax.transData, - size, - str(size), - loc='lower center', - pad=0.1, borderpad=0.5, sep=5, - frameon=False) - ax.add_artist(asb) +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) + +# 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%") + +ax3 = fig.add_subplot(224) +axins3 = inset_axes(ax3, width="100%", height="100%", + bbox_to_anchor=(.7, .5, .3, .5), + bbox_transform=ax3.transAxes) + +# 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, (ax, ax2) = plt.subplots(1, 2, figsize=[5.5, 3]) -# first subplot -ax.set_aspect(1) +############################################################################### +# In the above the axes transform together with 4-tuple bounding boxes has been +# used as it mostly is useful to specify an inset relative to the axes it is +# an inset to. However other use cases are equally possible. The following +# example examines some of those. +# -axins = inset_axes(ax, - width="30%", # width = 30% of parent_bbox - height=1., # height : 1 inch - loc='lower left') +fig = plt.figure(figsize=[5.5, 2.8]) +ax = fig.add_subplot(131) -plt.xticks(visible=False) -plt.yticks(visible=False) +# 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.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) -# second subplot -ax2.set_aspect(1) -axins = zoomed_inset_axes(ax2, zoom=0.5, loc='upper right') -# fix the number of ticks on the inset axes -axins.yaxis.get_major_locator().set_params(nbins=7) -axins.xaxis.get_major_locator().set_params(nbins=7) +ax2 = fig.add_subplot(133) +ax2.set_xscale("log") +ax2.axis([1e-6, 1e6, -2, 6]) -plt.xticks(visible=False) -plt.yticks(visible=False) +# 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) -add_sizebar(ax2, 0.5) -add_sizebar(axins, 0.5) +# 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) -plt.draw() plt.show() diff --git a/examples/axes_grid1/inset_locator_demo2.py b/examples/axes_grid1/inset_locator_demo2.py index 4e50dd5855c5..509413d3bf83 100644 --- a/examples/axes_grid1/inset_locator_demo2.py +++ b/examples/axes_grid1/inset_locator_demo2.py @@ -3,11 +3,16 @@ Inset Locator Demo2 =================== +This Demo shows how to create a zoomed inset via `~.zoomed_inset_axes`. +In the first subplot an `~.AnchoredSizeBar` shows the zoom effect. +In the second subplot a connection to the region of interest is +created via `~.mark_inset`. """ + import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes -from mpl_toolkits.axes_grid1.inset_locator import mark_inset +from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes, mark_inset +from mpl_toolkits.axes_grid1.anchored_artists import AnchoredSizeBar import numpy as np @@ -20,36 +25,63 @@ def get_demo_image(): # z is a numpy array of 15x15 return z, (-3, 4, -4, 3) -fig, ax = plt.subplots(figsize=[5, 4]) +fig, (ax, ax2) = plt.subplots(ncols=2, figsize=[6, 3]) + + +# First subplot, showing an inset with a size bar. +ax.set_aspect(1) + +axins = zoomed_inset_axes(ax, zoom=0.5, loc='upper right') +# fix the number of ticks on the inset axes +axins.yaxis.get_major_locator().set_params(nbins=7) +axins.xaxis.get_major_locator().set_params(nbins=7) + +plt.setp(axins.get_xticklabels(), visible=False) +plt.setp(axins.get_yticklabels(), visible=False) -# prepare the demo image + +def add_sizebar(ax, size): + asb = AnchoredSizeBar(ax.transData, + size, + str(size), + loc=8, + pad=0.1, borderpad=0.5, sep=5, + frameon=False) + ax.add_artist(asb) + +add_sizebar(ax, 0.5) +add_sizebar(axins, 0.5) + + +# Second subplot, showing an image with an inset zoom +# and a marked inset Z, extent = get_demo_image() Z2 = np.zeros([150, 150], dtype="d") ny, nx = Z.shape Z2[30:30 + ny, 30:30 + nx] = Z # extent = [-3, 4, -4, 3] -ax.imshow(Z2, extent=extent, interpolation="nearest", +ax2.imshow(Z2, extent=extent, interpolation="nearest", origin="lower") -axins = zoomed_inset_axes(ax, zoom=6, loc='upper right') -axins.imshow(Z2, extent=extent, interpolation="nearest", - origin="lower") + +axins2 = zoomed_inset_axes(ax2, 6, loc=1) # zoom = 6 +axins2.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) +axins2.set_xlim(x1, x2) +axins2.set_ylim(y1, y2) # fix the number of ticks on the inset axes -axins.yaxis.get_major_locator().set_params(nbins=7) -axins.xaxis.get_major_locator().set_params(nbins=7) +axins2.yaxis.get_major_locator().set_params(nbins=7) +axins2.xaxis.get_major_locator().set_params(nbins=7) -plt.xticks(visible=False) -plt.yticks(visible=False) +plt.setp(axins2.get_xticklabels(), visible=False) +plt.setp(axins2.get_yticklabels(), visible=False) # draw a bbox of the region of the inset axes in the parent axes and # connecting lines between the bbox and the inset axes area -mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5") +mark_inset(ax2, axins2, loc1=2, loc2=4, fc="none", ec="0.5") -plt.draw() plt.show() diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 75a6a90d7bb7..dfdb1a67a105 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -1,6 +1,8 @@ """ A collection of functions and objects for creating or placing inset axes. """ + +import warnings from matplotlib import docstring import six from matplotlib.offsetbox import AnchoredOffsetbox @@ -391,8 +393,16 @@ def inset_axes(parent_axes, width, height, loc='upper right', """ Create an inset axes with a given width and height. - Both sizes used can be specified either in inches or percentage of the - parent axes. + Both sizes used can be specified either in inches or percentage. + For example,:: + + inset_axes(parent_axes, width='40%%', height='30%%', loc=3) + + creates in inset axes in the lower left corner of *parent_axes* which spans + over 30%% in height and 40%% in width of the *parent_axes*. Since the usage + of `.inset_axes` may become slightly tricky when exceeding such standard + cases, it is recommended to read + :ref:`the examples `. Parameters ---------- @@ -400,7 +410,12 @@ def inset_axes(parent_axes, width, height, loc='upper right', Axes to place the inset axes. width, height : float or str - Size of the inset axes to create. + Size of the inset axes to create. If a float is provided, it is + the size in inches, e.g. *width=1.3*. If a string is provided, it is + the size in relative units, e.g. *width='40%%'*. By default, i.e. if + neither *bbox_to_anchor* nor *bbox_transform* are specified, those + are relative to the parent_axes. Otherwise they are to be understood + relative to the bounding box provided via *bbox_to_anchor*. loc : int or string, optional, default to 1 Location to place the inset axes. The valid locations are:: @@ -417,14 +432,29 @@ def inset_axes(parent_axes, width, height, loc='upper right', 'center' : 10 bbox_to_anchor : tuple or `matplotlib.transforms.BboxBase`, optional - Bbox that the inset axes will be anchored. Can be a tuple of - [left, bottom, width, height], or a tuple of [left, bottom]. + Bbox that the inset axes will be anchored to. If None, + *parent_axes.bbox* is used. If a tuple, can be either + [left, bottom, width, height], or [left, bottom]. + If the kwargs *width* and/or *height* are specified in relative units, + the 2-tuple [left, bottom] cannot be used. Note that + the units of the bounding box are determined through the transform + in use. When using *bbox_to_anchor* it almost always makes sense to + also specify a *bbox_transform*. This might often be the axes transform + *parent_axes.transAxes*. bbox_transform : `matplotlib.transforms.Transform`, optional - Transformation for the bbox. if None, `parent_axes.transAxes` is used. + Transformation for the bbox that contains the inset axes. + If None, a `.transforms.IdentityTransform` is used (i.e. pixel + coordinates). This is useful when not providing any argument to + *bbox_to_anchor*. When using *bbox_to_anchor* it almost always makes + sense to also specify a *bbox_transform*. This might often be the + axes transform *parent_axes.transAxes*. Inversely, when specifying + the axes- or figure-transform here, be aware that not specifying + *bbox_to_anchor* will use *parent_axes.bbox*, the units of which are + in display (pixel) coordinates. axes_class : `matplotlib.axes.Axes` type, optional - If specified, the inset axes created with be created with this class's + If specified, the inset axes created will be created with this class's constructor. axes_kwargs : dict, optional @@ -434,6 +464,8 @@ def inset_axes(parent_axes, width, height, loc='upper right', borderpad : float, optional Padding between inset axes and the bbox_to_anchor. Defaults to 0.5. + The units are axes font size, i.e. for a default font size of 10 points + *borderpad = 0.5* is equivalent to a padding of 5 points. Returns ------- @@ -450,11 +482,23 @@ def inset_axes(parent_axes, width, height, loc='upper right', inset_axes = axes_class(parent_axes.figure, parent_axes.get_position(), **axes_kwargs) + if bbox_transform in [parent_axes.transAxes, + parent_axes.figure.transFigure]: + if bbox_to_anchor is None: + warnings.warn("Using the axes or figure transform requires a " + "bounding box in the respective coordinates. " + "Using bbox_to_anchor=(0,0,1,1) now.") + bbox_to_anchor = (0, 0, 1, 1) + if bbox_to_anchor is None: bbox_to_anchor = parent_axes.bbox - if bbox_transform is None: - bbox_transform = parent_axes.transAxes + if isinstance(bbox_to_anchor, tuple) and \ + (isinstance(width, str) or isinstance(height, str)): + if len(bbox_to_anchor) != 4: + raise ValueError("Using relative units for width or height " + "requires to provide a 4-tuple or a " + "`BBox` instance to `bbox_to_anchor.") axes_locator = AnchoredSizeLocator(bbox_to_anchor, width, height, @@ -476,7 +520,8 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', axes_kwargs=None, borderpad=0.5): """ - Create an anchored inset axes by scaling a parent axes. + Create an anchored inset axes by scaling a parent axes. For usage, also see + :ref:`the examples `. Parameters ---------- @@ -503,14 +548,29 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', 'center' : 10 bbox_to_anchor : tuple or `matplotlib.transforms.BboxBase`, optional - Bbox that the inset axes will be anchored. Can be a tuple of - [left, bottom, width, height], or a tuple of [left, bottom]. + Bbox that the inset axes will be anchored to. If None, + *parent_axes.bbox* is used. If a tuple, can be either + [left, bottom, width, height], or [left, bottom]. + If the kwargs *width* and/or *height* are specified in relative units, + the 2-tuple [left, bottom] cannot be used. Note that + the units of the bounding box are determined through the transform + in use. When using *bbox_to_anchor* it almost always makes sense to + also specify a *bbox_transform*. This might often be the axes transform + *parent_axes.transAxes*. bbox_transform : `matplotlib.transforms.Transform`, optional - Transformation for the bbox. if None, `parent_axes.transAxes` is used. + Transformation for the bbox that contains the inset axes. + If None, a `.transforms.IdentityTransform` is used (i.e. pixel + coordinates). This is useful when not providing any argument to + *bbox_to_anchor*. When using *bbox_to_anchor* it almost always makes + sense to also specify a *bbox_transform*. This might often be the + axes transform *parent_axes.transAxes*. Inversely, when specifying + the axes- or figure-transform here, be aware that not specifying + *bbox_to_anchor* will use *parent_axes.bbox*, the units of which are + in display (pixel) coordinates. axes_class : `matplotlib.axes.Axes` type, optional - If specified, the inset axes created with be created with this class's + If specified, the inset axes created will be created with this class's constructor. axes_kwargs : dict, optional @@ -520,6 +580,8 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', borderpad : float, optional Padding between inset axes and the bbox_to_anchor. Defaults to 0.5. + The units are axes font size, i.e. for a default font size of 10 points + *borderpad = 0.5* is equivalent to a padding of 5 points. Returns ------- diff --git a/lib/mpl_toolkits/tests/test_axes_grid1.py b/lib/mpl_toolkits/tests/test_axes_grid1.py index 685f103930b9..6205e3ed4360 100644 --- a/lib/mpl_toolkits/tests/test_axes_grid1.py +++ b/lib/mpl_toolkits/tests/test_axes_grid1.py @@ -22,7 +22,10 @@ blended_transform_factory from itertools import product +import pytest + import numpy as np +from numpy.testing import assert_array_equal, assert_array_almost_equal @image_comparison(baseline_images=['divider_append_axes']) @@ -186,8 +189,9 @@ def get_demo_image(): ax.imshow(Z2, extent=extent, interpolation="nearest", origin="lower") - # creating our inset axes without a bbox_transform parameter - axins = inset_axes(ax, width=1., height=1., bbox_to_anchor=(1, 1)) + # creating our inset axes with a bbox_transform parameter + axins = inset_axes(ax, width=1., height=1., bbox_to_anchor=(1, 1), + bbox_transform=ax.transAxes) axins.imshow(Z2, extent=extent, interpolation="nearest", origin="lower") @@ -214,19 +218,48 @@ def get_demo_image(): ax.add_artist(asb) -def test_inset_axes_without_transform_should_use_parent_axes(): - # creating our figure - fig = plt.figure(dpi=150) - - # gca method gets current axes of the figure - ax = plt.gca() - ax.plot([0.0, 0.25, 0.50, 1.0], [0.1, 0.2, 0.4, 0.9], color='b') - - # creating our inset_axes. without a bbox_transform parameter - ax_ins = inset_axes(ax, width=1., height=1., bbox_to_anchor=(1, 1)) - ax_ins.plot([0.0, 0.25, 0.50, 1.0], [0.9, 0.4, 0.2, 0.1], color='r') - - assert ax.transAxes == ax_ins.transAxes +def test_inset_axes_complete(): + dpi = 100 + figsize = (6, 5) + fig, ax = plt.subplots(figsize=figsize, dpi=dpi) + fig.subplots_adjust(.1, .1, .9, .9) + + ins = inset_axes(ax, width=2., height=2., borderpad=0) + fig.canvas.draw() + assert_array_almost_equal( + ins.get_position().extents, + np.array(((0.9*figsize[0]-2.)/figsize[0], + (0.9*figsize[1]-2.)/figsize[1], 0.9, 0.9))) + + ins = inset_axes(ax, width="40%", height="30%", borderpad=0) + fig.canvas.draw() + assert_array_almost_equal( + ins.get_position().extents, + np.array((.9-.8*.4, .9-.8*.3, 0.9, 0.9))) + + ins = inset_axes(ax, width=1., height=1.2, bbox_to_anchor=(200, 100), + loc=3, borderpad=0) + fig.canvas.draw() + assert_array_almost_equal( + ins.get_position().extents, + np.array((200./dpi/figsize[0], 100./dpi/figsize[1], + (200./dpi+1)/figsize[0], (100./dpi+1.2)/figsize[1]))) + + ins1 = inset_axes(ax, width="35%", height="60%", loc=3, borderpad=1) + ins2 = inset_axes(ax, width="100%", height="100%", + bbox_to_anchor=(0, 0, .35, .60), + bbox_transform=ax.transAxes, loc=3, borderpad=1) + fig.canvas.draw() + assert_array_equal(ins1.get_position().extents, + ins2.get_position().extents) + + with pytest.raises(ValueError): + ins = inset_axes(ax, width="40%", height="30%", + bbox_to_anchor=(0.4, 0.5)) + + with pytest.warns(UserWarning): + ins = inset_axes(ax, width="40%", height="30%", + bbox_transform=ax.transAxes) @image_comparison(