Skip to content

Commit db45c2f

Browse files
box aspect for axes
1 parent 662bb8c commit db45c2f

File tree

5 files changed

+318
-4
lines changed

5 files changed

+318
-4
lines changed

doc/api/axes_api.rst

+3
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,9 @@ Aspect ratio
369369
Axes.set_aspect
370370
Axes.get_aspect
371371

372+
Axes.set_box_aspect
373+
Axes.get_box_aspect
374+
372375
Axes.set_adjustable
373376
Axes.get_adjustable
374377

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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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+
# Box aspect for many subplots
135+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
136+
#
137+
# It is possible to pass the box aspect to an axes at initialization. The
138+
# following creates a 2 by 3 subplot grid with all square axes.
139+
140+
fig7, axs = plt.subplots(2, 3, subplot_kw=dict(box_aspect=1),
141+
sharex=True, sharey=True, constrained_layout=True)
142+
143+
for i, ax in enumerate(axs.flat):
144+
ax.scatter(i % 3, -((i // 3) - 0.5)*200, c=[plt.cm.hsv(i / 6)], s=300)
145+
plt.show()
146+
147+
#############################################################################
148+
#
149+
# ------------
150+
#
151+
# References
152+
# """"""""""
153+
#
154+
# The use of the following functions, methods and classes is shown
155+
# in this example:
156+
157+
matplotlib.axes.Axes.set_box_aspect

lib/matplotlib/axes/_base.py

+79-4
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ def __init__(self, fig, rect,
383383
label='',
384384
xscale=None,
385385
yscale=None,
386+
box_aspect=None,
386387
**kwargs
387388
):
388389
"""
@@ -404,6 +405,10 @@ def __init__(self, fig, rect,
404405
frameon : bool, optional
405406
True means that the axes frame is visible.
406407
408+
box_aspect : None, or a number, optional
409+
Sets the aspect of the axes box. See `~.axes.Axes.set_box_aspect`
410+
for details.
411+
407412
**kwargs
408413
Other optional keyword arguments:
409414
@@ -437,7 +442,7 @@ def __init__(self, fig, rect,
437442
self._shared_y_axes.join(self, sharey)
438443
self.set_label(label)
439444
self.set_figure(fig)
440-
445+
self.set_box_aspect(box_aspect)
441446
self.set_axes_locator(kwargs.get("axes_locator", None))
442447

443448
self.spines = self._gen_axes_spines()
@@ -1282,6 +1287,18 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
12821287
self.stale = True
12831288

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

12871304
def set_adjustable(self, adjustable, share=False):
@@ -1333,6 +1350,55 @@ def set_adjustable(self, adjustable, share=False):
13331350
ax._adjustable = adjustable
13341351
self.stale = True
13351352

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

14631529
aspect = self.get_aspect()
14641530

1465-
if aspect == 'auto':
1531+
if aspect == 'auto' and self._box_aspect is None:
14661532
self._set_position(position, which='active')
14671533
return
14681534

@@ -1482,11 +1548,20 @@ def apply_aspect(self, position=None):
14821548
self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
14831549
return
14841550

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

14871559
# reset active to original in case it had been changed by prior use
14881560
# of 'box'
1489-
self._set_position(position, which='active')
1561+
if self._box_aspect is None:
1562+
self._set_position(position, which='active')
1563+
else:
1564+
position = pb1.anchored(self.get_anchor(), pb)
14901565

14911566
x_trf = self.xaxis.get_transform()
14921567
y_trf = self.yaxis.get_transform()

lib/matplotlib/tests/test_axes.py

+65
Original file line numberDiff line numberDiff line change
@@ -6541,5 +6541,70 @@ def test_aspect_nonlinear_adjustable_datalim():
65416541
aspect=1, adjustable="datalim")
65426542
ax.margins(0)
65436543
ax.apply_aspect()
6544+
65446545
assert ax.get_xlim() == pytest.approx([1*10**(1/2), 100/10**(1/2)])
65456546
assert ax.get_ylim() == (1 / 101, 1 / 11)
6547+
6548+
6549+
def test_box_aspect():
6550+
# Test if axes with box_aspect=1 has same dimensions
6551+
# as axes with aspect equal and adjustable="box"
6552+
6553+
fig1, ax1 = plt.subplots()
6554+
axtwin = ax1.twinx()
6555+
axtwin.plot([12, 344])
6556+
6557+
ax1.set_box_aspect(1)
6558+
6559+
fig2, ax2 = plt.subplots()
6560+
ax2.margins(0)
6561+
ax2.plot([0, 2], [6, 8])
6562+
ax2.set_aspect("equal", adjustable="box")
6563+
6564+
fig1.canvas.draw()
6565+
fig2.canvas.draw()
6566+
6567+
bb1 = ax1.get_position()
6568+
bbt = axtwin.get_position()
6569+
bb2 = ax2.get_position()
6570+
6571+
assert_array_equal(bb1.extents, bb2.extents)
6572+
assert_array_equal(bbt.extents, bb2.extents)
6573+
6574+
6575+
def test_box_aspect_custom_position():
6576+
# Test if axes with custom position and box_aspect
6577+
# behaves the same independent of the order of setting those.
6578+
6579+
fig1, ax1 = plt.subplots()
6580+
ax1.set_position([0.1, 0.1, 0.9, 0.2])
6581+
fig1.canvas.draw()
6582+
ax1.set_box_aspect(1.)
6583+
6584+
fig2, ax2 = plt.subplots()
6585+
ax2.set_box_aspect(1.)
6586+
fig2.canvas.draw()
6587+
ax2.set_position([0.1, 0.1, 0.9, 0.2])
6588+
6589+
fig1.canvas.draw()
6590+
fig2.canvas.draw()
6591+
6592+
bb1 = ax1.get_position()
6593+
bb2 = ax2.get_position()
6594+
6595+
assert_array_equal(bb1.extents, bb2.extents)
6596+
6597+
6598+
def test_bbox_aspect_axes_init():
6599+
# Test that box_aspect can be given to axes init and produces
6600+
# all equal square axes.
6601+
fig, axs = plt.subplots(2, 3, subplot_kw=dict(box_aspect=1),
6602+
constrained_layout=True)
6603+
fig.canvas.draw()
6604+
renderer = fig.canvas.get_renderer()
6605+
sizes = []
6606+
for ax in axs.flat:
6607+
bb = ax.get_window_extent(renderer)
6608+
sizes.extend([bb.width, bb.height])
6609+
6610+
assert_allclose(sizes, sizes[0])

0 commit comments

Comments
 (0)