| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent fe7fa10 commit 458ff92
4 files changed
| Original file line number | Diff line number | Diff 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 number | Diff line number | Diff 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 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -383,6 +383,7 @@ def __init__(self, fig, rect, | |||
| 383 | 383 | label='', | |
| 384 | 384 | xscale=None, | |
| 385 | 385 | yscale=None, | |
| 386 | + box_aspect=None, | ||
| 386 | 387 | **kwargs | |
| 387 | 388 | ): | |
| 388 | 389 | """ | |
@@ -404,6 +405,10 @@ def __init__(self, fig, rect, | |||
| 404 | 405 | frameon : bool, optional | |
| 405 | 406 | True means that the axes frame is visible. | |
| 406 | 407 | ||
| 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 | + | ||
| 407 | 412 | **kwargs | |
| 408 | 413 | Other optional keyword arguments: | |
| 409 | 414 | ||
@@ -437,7 +442,7 @@ def __init__(self, fig, rect, | |||
| 437 | 442 | self._shared_y_axes.join(self, sharey) | |
| 438 | 443 | self.set_label(label) | |
| 439 | 444 | self.set_figure(fig) | |
| 440 | - | ||
| 445 | + self.set_box_aspect(box_aspect) | ||
| 441 | 446 | self.set_axes_locator(kwargs.get("axes_locator", None)) | |
| 442 | 447 | ||
| 443 | 448 | self.spines = self._gen_axes_spines() | |
@@ -1282,6 +1287,18 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): | |||
| 1282 | 1287 | self.stale = True | |
| 1283 | 1288 | ||
| 1284 | 1289 | 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 | + """ | ||
| 1285 | 1302 | return self._adjustable | |
| 1286 | 1303 | ||
| 1287 | 1304 | def set_adjustable(self, adjustable, share=False): | |
@@ -1333,6 +1350,55 @@ def set_adjustable(self, adjustable, share=False): | |||
| 1333 | 1350 | ax._adjustable = adjustable | |
| 1334 | 1351 | self.stale = True | |
| 1335 | 1352 | ||
| 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 | + | ||
| 1336 | 1402 | def get_anchor(self): | |
| 1337 | 1403 | """ | |
| 1338 | 1404 | Get the anchor location. | |
@@ -1462,7 +1528,7 @@ def apply_aspect(self, position=None): | |||
| 1462 | 1528 | ||
| 1463 | 1529 | aspect = self.get_aspect() | |
| 1464 | 1530 | ||
| 1465 | - if aspect == 'auto': | ||
| 1531 | + if aspect == 'auto' and self._box_aspect is None: | ||
| 1466 | 1532 | self._set_position(position, which='active') | |
| 1467 | 1533 | return | |
| 1468 | 1534 | ||
@@ -1482,11 +1548,20 @@ def apply_aspect(self, position=None): | |||
| 1482 | 1548 | self._set_position(pb1.anchored(self.get_anchor(), pb), 'active') | |
| 1483 | 1549 | return | |
| 1484 | 1550 | ||
| 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 | ||
| 1486 | 1558 | ||
| 1487 | 1559 | # reset active to original in case it had been changed by prior use | |
| 1488 | 1560 | # 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) | ||
| 1490 | 1565 | ||
| 1491 | 1566 | x_trf = self.xaxis.get_transform() | |
| 1492 | 1567 | y_trf = self.yaxis.get_transform() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6565,5 +6565,70 @@ def test_aspect_nonlinear_adjustable_datalim(): | |||
| 6565 | 6565 | aspect=1, adjustable="datalim") | |
| 6566 | 6566 | ax.margins(0) | |
| 6567 | 6567 | ax.apply_aspect() | |
| 6568 | + | ||
| 6568 | 6569 | assert ax.get_xlim() == pytest.approx([1*10**(1/2), 100/10**(1/2)]) | |
| 6569 | 6570 | 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 | + | ||
| 6621 | + | ||
| 6622 | + def test_bbox_aspect_axes_init(): | ||
| 6623 | + # Test that box_aspect can be given to axes init and produces | ||
| 6624 | + # all equal square axes. | ||
| 6625 | + fig, axs = plt.subplots(2, 3, subplot_kw=dict(box_aspect=1), | ||
| 6626 | + constrained_layout=True) | ||
| 6627 | + fig.canvas.draw() | ||
| 6628 | + renderer = fig.canvas.get_renderer() | ||
| 6629 | + sizes = [] | ||
| 6630 | + for ax in axs.flat: | ||
| 6631 | + bb = ax.get_window_extent(renderer) | ||
| 6632 | + sizes.extend([bb.width, bb.height]) | ||
| 6633 | + | ||
| 6634 | + assert_allclose(sizes, sizes[0]) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments