Skip to content

Commit 68af0c2

Browse files
box aspect for axes
1 parent 1f003b0 commit 68af0c2

File tree

4 files changed

+281
-3
lines changed

4 files changed

+281
-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.
@@ -1462,7 +1523,7 @@ def apply_aspect(self, position=None):
14621523

14631524
aspect = self.get_aspect()
14641525

1465-
if aspect == 'auto':
1526+
if aspect == 'auto' and self._box_aspect is None:
14661527
self._set_position(position, which='active')
14671528
return
14681529

@@ -1482,11 +1543,20 @@ def apply_aspect(self, position=None):
14821543
self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
14831544
return
14841545

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

14871554
# reset active to original in case it had been changed by prior use
14881555
# of 'box'
1489-
self._set_position(position, which='active')
1556+
if self._box_aspect is None:
1557+
self._set_position(position, which='active')
1558+
else:
1559+
position = pb1.anchored(self.get_anchor(), pb)
14901560

14911561
x_trf = self.xaxis.get_transform()
14921562
y_trf = self.yaxis.get_transform()

lib/matplotlib/tests/test_axes.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6565,5 +6565,56 @@ def test_aspect_nonlinear_adjustable_datalim():
65656565
aspect=1, adjustable="datalim")
65666566
ax.margins(0)
65676567
ax.apply_aspect()
6568+
65686569
assert ax.get_xlim() == pytest.approx([1*10**(1/2), 100/10**(1/2)])
65696570
assert ax.get_ylim() == (1 / 101, 1 / 11)
6571+
6572+
6573+
def test_box_aspect():
6574+
# Test if axes with box_aspect=1 has same dimensions
6575+
# as axes with aspect equal and adjustable="box"
6576+
6577+
fig1, ax1 = plt.subplots()
6578+
axtwin = ax1.twinx()
6579+
axtwin.plot([12, 344])
6580+
6581+
ax1.set_box_aspect(1)
6582+
6583+
fig2, ax2 = plt.subplots()
6584+
ax2.margins(0)
6585+
ax2.plot([0, 2], [6, 8])
6586+
ax2.set_aspect("equal", adjustable="box")
6587+
6588+
fig1.canvas.draw()
6589+
fig2.canvas.draw()
6590+
6591+
bb1 = ax1.get_position()
6592+
bbt = axtwin.get_position()
6593+
bb2 = ax2.get_position()
6594+
6595+
assert_array_equal(bb1.extents, bb2.extents)
6596+
assert_array_equal(bbt.extents, bb2.extents)
6597+
6598+
6599+
def test_box_aspect_custom_position():
6600+
# Test if axes with custom position and box_aspect
6601+
# behaves the same independent of the order of setting those.
6602+
6603+
fig1, ax1 = plt.subplots()
6604+
ax1.set_position([0.1, 0.1, 0.9, 0.2])
6605+
fig1.canvas.draw()
6606+
ax1.set_box_aspect(1.)
6607+
6608+
fig2, ax2 = plt.subplots()
6609+
ax2.set_box_aspect(1.)
6610+
fig2.canvas.draw()
6611+
ax2.set_position([0.1, 0.1, 0.9, 0.2])
6612+
6613+
fig1.canvas.draw()
6614+
fig2.canvas.draw()
6615+
6616+
bb1 = ax1.get_position()
6617+
bb2 = ax2.get_position()
6618+
6619+
assert_array_equal(bb1.extents, bb2.extents)
6620+

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