diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index a3660fac3ba7..20b775d11a62 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -369,6 +369,9 @@ Aspect ratio Axes.set_aspect Axes.get_aspect + Axes.set_box_aspect + Axes.get_box_aspect + Axes.set_adjustable Axes.get_adjustable diff --git a/doc/users/next_whats_new/2019-07-31_axes-box-aspect.rst b/doc/users/next_whats_new/2019-07-31_axes-box-aspect.rst new file mode 100644 index 000000000000..ca7650fbd496 --- /dev/null +++ b/doc/users/next_whats_new/2019-07-31_axes-box-aspect.rst @@ -0,0 +1,14 @@ +:orphan: + +Setting axes box aspect +----------------------- + +It is now possible to set the aspect of an axes box directly via +`~.Axes.set_box_aspect`. The box aspect is the ratio between axes height +and axes width in physical units, independent of the data limits. +This is useful to e.g. produce a square plot, independent of the data it +contains, or to have a usual plot with the same axes dimensions next to +an image plot with fixed (data-)aspect. + +For use cases check out the :doc:`Axes box aspect +` example. diff --git a/examples/subplots_axes_and_figures/axes_box_aspect.py b/examples/subplots_axes_and_figures/axes_box_aspect.py new file mode 100644 index 000000000000..862a979fa3e1 --- /dev/null +++ b/examples/subplots_axes_and_figures/axes_box_aspect.py @@ -0,0 +1,157 @@ +""" +=============== +Axes box aspect +=============== + +This demo shows how to set the aspect of an axes box directly via +`~.Axes.set_box_aspect`. The box aspect is the ratio between axes height +and axes width in physical units, independent of the data limits. +This is useful to e.g. produce a square plot, independent of the data it +contains, or to have a usual plot with the same axes dimensions next to +an image plot with fixed (data-)aspect. + +The following lists a few use cases for `~.Axes.set_box_aspect`. +""" + +############################################################################ +# A square axes, independent of data +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# Produce a square axes, no matter what the data limits are. + +import matplotlib +import numpy as np +import matplotlib.pyplot as plt + +fig1, ax = plt.subplots() + +ax.set_xlim(300, 400) +ax.set_box_aspect(1) + +plt.show() + +############################################################################ +# Shared square axes +# ~~~~~~~~~~~~~~~~~~ +# +# Produce shared subplots that are squared in size. +# +fig2, (ax, ax2) = plt.subplots(ncols=2, sharey=True) + +ax.plot([1, 5], [0, 10]) +ax2.plot([100, 500], [10, 15]) + +ax.set_box_aspect(1) +ax2.set_box_aspect(1) + +plt.show() + +############################################################################ +# Square twin axes +# ~~~~~~~~~~~~~~~~ +# +# Produce a square axes, with a twin axes. The twinned axes takes over the +# box aspect of the parent. +# + +fig3, ax = plt.subplots() + +ax2 = ax.twinx() + +ax.plot([0, 10]) +ax2.plot([12, 10]) + +ax.set_box_aspect(1) + +plt.show() + + +############################################################################ +# Normal plot next to image +# ~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# When creating an image plot with fixed data aspect and the default +# ``adjustable="box"`` next to a normal plot, the axes would be unequal in +# height. `~.Axes.set_box_aspect` provides an easy solution to that by allowing +# to have the normal plot's axes use the images dimensions as box aspect. +# +# This example also shows that ``constrained_layout`` interplays nicely with +# a fixed box aspect. + +fig4, (ax, ax2) = plt.subplots(ncols=2, constrained_layout=True) + +im = np.random.rand(16, 27) +ax.imshow(im) + +ax2.plot([23, 45]) +ax2.set_box_aspect(im.shape[0]/im.shape[1]) + +plt.show() + +############################################################################ +# Square joint/marginal plot +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# It may be desireable to show marginal distributions next to a plot of joint +# data. The following creates a square plot with the box aspect of the +# marginal axes being equal to the width- and height-ratios of the gridspec. +# This ensures that all axes align perfectly, independent on the size of the +# figure. + +fig5, axs = plt.subplots(2, 2, sharex="col", sharey="row", + gridspec_kw=dict(height_ratios=[1, 3], + width_ratios=[3, 1])) +axs[0, 1].set_visible(False) +axs[0, 0].set_box_aspect(1/3) +axs[1, 0].set_box_aspect(1) +axs[1, 1].set_box_aspect(3/1) + +x, y = np.random.randn(2, 400) * np.array([[.5], [180]]) +axs[1, 0].scatter(x, y) +axs[0, 0].hist(x) +axs[1, 1].hist(y, orientation="horizontal") + +plt.show() + +############################################################################ +# Square joint/marginal plot +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# When setting the box aspect, one may still set the data aspect as well. +# Here we create an axes with a box twice as long as tall and use an "equal" +# data aspect for its contents, i.e. the circle actually stays circular. + +fig6, ax = plt.subplots() + +ax.add_patch(plt.Circle((5, 3), 1)) +ax.set_aspect("equal", adjustable="datalim") +ax.set_box_aspect(0.5) +ax.autoscale() + +plt.show() + +############################################################################ +# Box aspect for many subplots +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# It is possible to pass the box aspect to an axes at initialization. The +# following creates a 2 by 3 subplot grid with all square axes. + +fig7, axs = plt.subplots(2, 3, subplot_kw=dict(box_aspect=1), + sharex=True, sharey=True, constrained_layout=True) + +for i, ax in enumerate(axs.flat): + ax.scatter(i % 3, -((i // 3) - 0.5)*200, c=[plt.cm.hsv(i / 6)], s=300) +plt.show() + +############################################################################# +# +# ------------ +# +# References +# """""""""" +# +# The use of the following functions, methods and classes is shown +# in this example: + +matplotlib.axes.Axes.set_box_aspect diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 117adc9d0c1b..41658a7018ae 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -383,6 +383,7 @@ def __init__(self, fig, rect, label='', xscale=None, yscale=None, + box_aspect=None, **kwargs ): """ @@ -404,6 +405,10 @@ def __init__(self, fig, rect, frameon : bool, optional True means that the axes frame is visible. + box_aspect : None, or a number, optional + Sets the aspect of the axes box. See `~.axes.Axes.set_box_aspect` + for details. + **kwargs Other optional keyword arguments: @@ -437,7 +442,7 @@ def __init__(self, fig, rect, self._shared_y_axes.join(self, sharey) self.set_label(label) self.set_figure(fig) - + self.set_box_aspect(box_aspect) self.set_axes_locator(kwargs.get("axes_locator", None)) self.spines = self._gen_axes_spines() @@ -1282,6 +1287,18 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): self.stale = True def get_adjustable(self): + """ + Returns the adjustable parameter, *{'box', 'datalim'}* that defines + which parameter the Axes will change to achieve a given aspect. + + See Also + -------- + matplotlib.axes.Axes.set_adjustable + defining the parameter to adjust in order to meet the required + aspect. + matplotlib.axes.Axes.set_aspect + for a description of aspect handling. + """ return self._adjustable def set_adjustable(self, adjustable, share=False): @@ -1333,6 +1350,55 @@ def set_adjustable(self, adjustable, share=False): ax._adjustable = adjustable self.stale = True + def get_box_aspect(self): + """ + Get the axes box aspect. + Will be ``None`` if not explicitely specified. + + See Also + -------- + matplotlib.axes.Axes.set_box_aspect + for a description of box aspect. + matplotlib.axes.Axes.set_aspect + for a description of aspect handling. + """ + return self._box_aspect + + def set_box_aspect(self, aspect=None): + """ + Set the axes box aspect. The box aspect is the ratio of the + axes height to the axes width in physical units. This is not to be + confused with the data aspect, set via `~Axes.set_aspect`. + + Parameters + ---------- + aspect : None, or a number + Changes the physical dimensions of the Axes, such that the ratio + of the axes height to the axes width in physical units is equal to + *aspect*. If *None*, the axes geometry will not be adjusted. + + Note that calling this function with a number changes the *adjustable* + to *datalim*. + + See Also + -------- + matplotlib.axes.Axes.set_aspect + for a description of aspect handling. + """ + axs = {*self._twinned_axes.get_siblings(self), + *self._twinned_axes.get_siblings(self)} + + if aspect is not None: + aspect = float(aspect) + # when box_aspect is set to other than ´None`, + # adjustable must be "datalim" + for ax in axs: + ax.set_adjustable("datalim") + + for ax in axs: + ax._box_aspect = aspect + ax.stale = True + def get_anchor(self): """ Get the anchor location. @@ -1462,7 +1528,7 @@ def apply_aspect(self, position=None): aspect = self.get_aspect() - if aspect == 'auto': + if aspect == 'auto' and self._box_aspect is None: self._set_position(position, which='active') return @@ -1482,11 +1548,20 @@ def apply_aspect(self, position=None): self._set_position(pb1.anchored(self.get_anchor(), pb), 'active') return - # self._adjustable == 'datalim' + # The following is only seen if self._adjustable == 'datalim' + if self._box_aspect is not None: + pb = position.frozen() + pb1 = pb.shrunk_to_aspect(self._box_aspect, pb, fig_aspect) + self._set_position(pb1.anchored(self.get_anchor(), pb), 'active') + if aspect == "auto": + return # reset active to original in case it had been changed by prior use # of 'box' - self._set_position(position, which='active') + if self._box_aspect is None: + self._set_position(position, which='active') + else: + position = pb1.anchored(self.get_anchor(), pb) x_trf = self.xaxis.get_transform() y_trf = self.yaxis.get_transform() diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index c36070442a4c..f2c33f90da69 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6546,5 +6546,70 @@ def test_aspect_nonlinear_adjustable_datalim(): aspect=1, adjustable="datalim") ax.margins(0) ax.apply_aspect() + assert ax.get_xlim() == pytest.approx([1*10**(1/2), 100/10**(1/2)]) assert ax.get_ylim() == (1 / 101, 1 / 11) + + +def test_box_aspect(): + # Test if axes with box_aspect=1 has same dimensions + # as axes with aspect equal and adjustable="box" + + fig1, ax1 = plt.subplots() + axtwin = ax1.twinx() + axtwin.plot([12, 344]) + + ax1.set_box_aspect(1) + + fig2, ax2 = plt.subplots() + ax2.margins(0) + ax2.plot([0, 2], [6, 8]) + ax2.set_aspect("equal", adjustable="box") + + fig1.canvas.draw() + fig2.canvas.draw() + + bb1 = ax1.get_position() + bbt = axtwin.get_position() + bb2 = ax2.get_position() + + assert_array_equal(bb1.extents, bb2.extents) + assert_array_equal(bbt.extents, bb2.extents) + + +def test_box_aspect_custom_position(): + # Test if axes with custom position and box_aspect + # behaves the same independent of the order of setting those. + + fig1, ax1 = plt.subplots() + ax1.set_position([0.1, 0.1, 0.9, 0.2]) + fig1.canvas.draw() + ax1.set_box_aspect(1.) + + fig2, ax2 = plt.subplots() + ax2.set_box_aspect(1.) + fig2.canvas.draw() + ax2.set_position([0.1, 0.1, 0.9, 0.2]) + + fig1.canvas.draw() + fig2.canvas.draw() + + bb1 = ax1.get_position() + bb2 = ax2.get_position() + + assert_array_equal(bb1.extents, bb2.extents) + + +def test_bbox_aspect_axes_init(): + # Test that box_aspect can be given to axes init and produces + # all equal square axes. + fig, axs = plt.subplots(2, 3, subplot_kw=dict(box_aspect=1), + constrained_layout=True) + fig.canvas.draw() + renderer = fig.canvas.get_renderer() + sizes = [] + for ax in axs.flat: + bb = ax.get_window_extent(renderer) + sizes.extend([bb.width, bb.height]) + + assert_allclose(sizes, sizes[0]) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy