Skip to content

Commit 0e2fdb9

Browse files
box aspect for axes
1 parent 667a100 commit 0e2fdb9

File tree

4 files changed

+279
-3
lines changed

4 files changed

+279
-3
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
:orphan:
2+
3+
Setting axes box aspect
4+
-----------------------
5+
6+
It is now possible to set the aspect of an axes box directly via
7+
`~Axes.set_box_aspect`. The box aspect is the ratio between axes height
8+
and axes width in physical units, independent of the data limits.
9+
This is useful to e.g. produce a square plot, independent of the data it
10+
contains, or to have a usual plot with the same axes dimensions next to
11+
an image plot with fixed (data-)aspect.
12+
13+
For use cases check out the :doc:`Axes box aspect
14+
</gallery/subplots_axes_and_figures/axes_box_aspect>` example.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""
2+
===============
3+
Axes box aspect
4+
===============
5+
6+
This demo shows how to set the aspect of an axes box directly via
7+
`~Axes.set_box_aspect`. The box aspect is the ratio between axes height
8+
and axes width in physical units, independent of the data limits.
9+
This is useful to e.g. produce a square plot, independent of the data it
10+
contains, or to have a usual plot with the same axes dimensions next to
11+
an image plot with fixed (data-)aspect.
12+
13+
The following lists a few use cases for `~Axes.set_box_aspect`.
14+
"""
15+
16+
############################################################################
17+
# A square axes, independent of data
18+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
19+
#
20+
# Produce a square axes, no matter what the data limits are.
21+
22+
import matplotlib
23+
import numpy as np
24+
import matplotlib.pyplot as plt
25+
26+
fig1, ax = plt.subplots()
27+
28+
ax.set_xlim(300, 400)
29+
ax.set_box_aspect(1)
30+
31+
plt.show()
32+
33+
############################################################################
34+
# Shared square axes
35+
# ~~~~~~~~~~~~~~~~~~
36+
#
37+
# Produce shared subplots that are squared in size.
38+
#
39+
fig2, (ax, ax2) = plt.subplots(ncols=2, sharey=True)
40+
41+
ax.plot([1, 5], [0, 10])
42+
ax2.plot([100, 500], [10, 15])
43+
44+
ax.set_box_aspect(1)
45+
ax2.set_box_aspect(1)
46+
47+
plt.show()
48+
49+
############################################################################
50+
# Square twin axes
51+
# ~~~~~~~~~~~~~~~~
52+
#
53+
# Produce a square axes, with a twin axes. The twinned axes takes over the
54+
# box aspect of the parent.
55+
#
56+
57+
fig3, ax = plt.subplots()
58+
59+
ax2 = ax.twinx()
60+
61+
ax.plot([0, 10])
62+
ax2.plot([12, 10])
63+
64+
ax.set_box_aspect(1)
65+
66+
plt.show()
67+
68+
69+
############################################################################
70+
# Normal plot next to image
71+
# ~~~~~~~~~~~~~~~~~~~~~~~~~
72+
#
73+
# When creating an image plot with fixed data aspect and the default
74+
# ``adjustable="box"`` next to a normal plot, the axes would be unequal in
75+
# height. `~Axes.set_box_aspect` provides an easy solution to that by allowing
76+
# to have the normal plot's axes use the images dimensions as box aspect.
77+
#
78+
# This example also shows that ``constrained_layout`` interplays nicely with
79+
# a fixed box aspect.
80+
81+
fig4, (ax, ax2) = plt.subplots(ncols=2, constrained_layout=True)
82+
83+
im = np.random.rand(16, 27)
84+
ax.imshow(im)
85+
86+
ax2.plot([23, 45])
87+
ax2.set_box_aspect(im.shape[0]/im.shape[1])
88+
89+
plt.show()
90+
91+
############################################################################
92+
# Square joint/marginal plot
93+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~
94+
#
95+
# It may be desireable to show marginal distributions next to a plot of joint
96+
# data. The following creates a square plot with the box aspect of the
97+
# marginal axes being equal to the width- and height-ratios of the gridspec.
98+
# This ensures that all axes align perfectly, independent on the size of the
99+
# figure.
100+
101+
fig5, axs = plt.subplots(2, 2, sharex="col", sharey="row",
102+
gridspec_kw=dict(height_ratios=[1, 3],
103+
width_ratios=[3, 1]))
104+
axs[0, 1].set_visible(False)
105+
axs[0, 0].set_box_aspect(1/3)
106+
axs[1, 0].set_box_aspect(1)
107+
axs[1, 1].set_box_aspect(3/1)
108+
109+
x, y = np.random.randn(2, 400) * np.array([[.5], [180]])
110+
axs[1, 0].scatter(x, y)
111+
axs[0, 0].hist(x)
112+
axs[1, 1].hist(y, orientation="horizontal")
113+
114+
plt.show()
115+
116+
############################################################################
117+
# Square joint/marginal plot
118+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~
119+
#
120+
# When setting the box aspect, one may still set the data aspect as well.
121+
# Here we create an axes with a box twice as long as tall and use an "equal"
122+
# data aspect for its contents, i.e. the circle actually stays circular.
123+
124+
fig6, ax = plt.subplots()
125+
126+
ax.add_patch(plt.Circle((5, 3), 1))
127+
ax.set_aspect("equal", adjustable="datalim")
128+
ax.set_box_aspect(0.5)
129+
ax.autoscale()
130+
131+
plt.show()
132+
133+
#############################################################################
134+
#
135+
# ------------
136+
#
137+
# References
138+
# """"""""""
139+
#
140+
# The use of the following functions, methods and classes is shown
141+
# in this example:
142+
143+
matplotlib.axes.Axes.set_box_aspect

lib/matplotlib/axes/_base.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ def __init__(self, fig, rect,
426426
self.axes = self
427427
self._aspect = 'auto'
428428
self._adjustable = 'box'
429+
self._box_aspect = None
429430
self._anchor = 'C'
430431
self._stale_viewlim_x = False
431432
self._stale_viewlim_y = False
@@ -1282,6 +1283,18 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
12821283
self.stale = True
12831284

12841285
def get_adjustable(self):
1286+
"""
1287+
Returns the adjustable parameter, *{'box', 'datalim'}* that defines
1288+
which parameter the Axes will change to achieve a given aspect.
1289+
1290+
See Also
1291+
--------
1292+
matplotlib.axes.Axes.set_adjustable
1293+
defining the parameter to adjust in order to meet the required
1294+
aspect.
1295+
matplotlib.axes.Axes.set_aspect
1296+
for a description of aspect handling.
1297+
"""
12851298
return self._adjustable
12861299

12871300
def set_adjustable(self, adjustable, share=False):
@@ -1333,6 +1346,54 @@ def set_adjustable(self, adjustable, share=False):
13331346
ax._adjustable = adjustable
13341347
self.stale = True
13351348

1349+
def get_box_aspect(self):
1350+
"""
1351+
Get the axes box aspect.
1352+
Will be ``None`` if not explicitely specified.
1353+
1354+
See Also
1355+
--------
1356+
matplotlib.axes.Axes.set_box_aspect
1357+
for a description of box aspect.
1358+
matplotlib.axes.Axes.set_aspect
1359+
for a description of aspect handling.
1360+
"""
1361+
return self._box_aspect
1362+
1363+
def set_box_aspect(self, aspect=None):
1364+
"""
1365+
Set the axes box aspect. The box aspect is the ratio of the
1366+
axes height to the axes width in physical units. This is not to be
1367+
confused with the data aspect, set via `~Axes.set_aspect`.
1368+
1369+
Parameters
1370+
----------
1371+
aspect : None, or a number
1372+
Changes the physical dimensions of the Axes, such that the ratio
1373+
of the axes height to the axes width in physical units is equal to
1374+
*aspect*. If *None*, the axes geometry will not be adjusted.
1375+
1376+
Note that this changes the adjustable to *datalim*.
1377+
1378+
See Also
1379+
--------
1380+
matplotlib.axes.Axes.set_aspect
1381+
for a description of aspect handling.
1382+
"""
1383+
axs = {*self._twinned_axes.get_siblings(self),
1384+
*self._twinned_axes.get_siblings(self)}
1385+
1386+
if aspect is not None:
1387+
aspect = float(aspect)
1388+
# when box_aspect is set to other than ´None`,
1389+
# adjustable must be "datalim"
1390+
for ax in axs:
1391+
ax.set_adjustable("datalim")
1392+
1393+
for ax in axs:
1394+
ax._box_aspect = aspect
1395+
ax.stale = True
1396+
13361397
def get_anchor(self):
13371398
"""
13381399
Get the anchor location.
@@ -1464,7 +1525,7 @@ def apply_aspect(self, position=None):
14641525

14651526
aspect = self.get_aspect()
14661527

1467-
if aspect == 'auto':
1528+
if aspect == 'auto' and self._box_aspect is None:
14681529
self._set_position(position, which='active')
14691530
return
14701531

@@ -1484,11 +1545,20 @@ def apply_aspect(self, position=None):
14841545
self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
14851546
return
14861547

1487-
# self._adjustable == 'datalim'
1548+
# The following is only seen if self._adjustable == 'datalim'
1549+
if self._box_aspect is not None:
1550+
pb = position.frozen()
1551+
pb1 = pb.shrunk_to_aspect(self._box_aspect, pb, fig_aspect)
1552+
self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
1553+
if aspect == "auto":
1554+
return
14881555

14891556
# reset active to original in case it had been changed by prior use
14901557
# of 'box'
1491-
self._set_position(position, which='active')
1558+
if self._box_aspect is None:
1559+
self._set_position(position, which='active')
1560+
else:
1561+
position = pb1.anchored(self.get_anchor(), pb)
14921562

14931563
x_trf = self.xaxis.get_transform()
14941564
y_trf = self.yaxis.get_transform()

lib/matplotlib/tests/test_axes.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6560,3 +6560,52 @@ def test_aspect_nonlinear_adjustable_datalim():
65606560
ax.margins(0)
65616561
ax.apply_aspect()
65626562
assert ax.get_xlim() == pytest.approx(np.array([1/10, 10]) * np.sqrt(10))
6563+
6564+
6565+
def test_box_aspect():
6566+
# Test if axes with box_aspect=1 has same dimensions
6567+
# as axes with aspect equal and adjustable="box"
6568+
6569+
fig1, ax1 = plt.subplots()
6570+
axtwin = ax1.twinx()
6571+
axtwin.plot([12, 344])
6572+
6573+
ax1.set_box_aspect(1)
6574+
6575+
fig2, ax2 = plt.subplots()
6576+
ax2.margins(0)
6577+
ax2.plot([0, 2], [6, 8])
6578+
ax2.set_aspect("equal", adjustable="box")
6579+
6580+
fig1.canvas.draw()
6581+
fig2.canvas.draw()
6582+
6583+
bb1 = ax1.get_position()
6584+
bbt = axtwin.get_position()
6585+
bb2 = ax2.get_position()
6586+
6587+
assert_array_equal(bb1.extents, bb2.extents)
6588+
assert_array_equal(bbt.extents, bb2.extents)
6589+
6590+
6591+
def test_box_aspect_custom_position():
6592+
# Test if axes with custom position and box_aspect
6593+
# behaves the same independent of the order of setting those.
6594+
6595+
fig1, ax1 = plt.subplots()
6596+
ax1.set_position([0.1, 0.1, 0.9, 0.2])
6597+
fig1.canvas.draw()
6598+
ax1.set_box_aspect(1.)
6599+
6600+
fig2, ax2 = plt.subplots()
6601+
ax2.set_box_aspect(1.)
6602+
fig2.canvas.draw()
6603+
ax2.set_position([0.1, 0.1, 0.9, 0.2])
6604+
6605+
fig1.canvas.draw()
6606+
fig2.canvas.draw()
6607+
6608+
bb1 = ax1.get_position()
6609+
bb2 = ax2.get_position()
6610+
6611+
assert_array_equal(bb1.extents, bb2.extents)

0 commit comments

Comments
 (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