|
| 1 | +""" |
| 2 | +========================================= |
| 3 | +Placing images, preserving relative sizes |
| 4 | +========================================= |
| 5 | +
|
| 6 | +By default Matplotlib resamples images created with `~.Axes.imshow` to |
| 7 | +fit inside the parent `~.axes.Axes`. This can mean that images that have very |
| 8 | +different original sizes can end up appearing similar in size. |
| 9 | +
|
| 10 | +Sometimes, however, it is desirable to keep the images the same relative size, or |
| 11 | +even to make the images keep exactly the same pixels as the original data. |
| 12 | +Matplotlib does not automatically make either of these things happen, |
| 13 | +but it is possible with some manual manipulation. |
| 14 | +
|
| 15 | +Preserving relative sizes |
| 16 | +========================= |
| 17 | +
|
| 18 | +By default the two images are made a similar size, despite one being 1.5 times the width |
| 19 | +of the other: |
| 20 | +""" |
| 21 | + |
| 22 | +# sphinx_gallery_thumbnail_number = -1 |
| 23 | + |
| 24 | +import matplotlib.pyplot as plt |
| 25 | +import numpy as np |
| 26 | + |
| 27 | +import matplotlib.patches as mpatches |
| 28 | + |
| 29 | +# make the data: |
| 30 | +N = 450 |
| 31 | +x = np.arange(N) / N |
| 32 | +y = np.arange(N) / N |
| 33 | + |
| 34 | +X, Y = np.meshgrid(x, y) |
| 35 | +R = np.sqrt(X**2 + Y**2) |
| 36 | +f0 = 5 |
| 37 | +k = 100 |
| 38 | +a = np.sin(np.pi * 2 * (f0 * R + k * R**2 / 2)) |
| 39 | +A = a[:100, :300] |
| 40 | +B = A[:40, :200] |
| 41 | + |
| 42 | +# plot with default axes handling: |
| 43 | +fig, axs = plt.subplots(1, 2, facecolor='aliceblue') |
| 44 | + |
| 45 | +axs[0].imshow(A, vmin=-1, vmax=1) |
| 46 | +axs[1].imshow(B, vmin=-1, vmax=1) |
| 47 | + |
| 48 | + |
| 49 | +def annotate_rect(ax): |
| 50 | + # add a rectangle that is the size of the B matrix |
| 51 | + rect = mpatches.Rectangle((0, 0), 200, 40, linewidth=1, |
| 52 | + edgecolor='r', facecolor='none') |
| 53 | + ax.add_patch(rect) |
| 54 | + return rect |
| 55 | + |
| 56 | +annotate_rect(axs[0]) |
| 57 | + |
| 58 | +# %% |
| 59 | +# Note that both images are rendered at a 1:1 ratio, but are made to look almost the |
| 60 | +# same width, despite image B being smaller than image A. |
| 61 | +# |
| 62 | +# If the size of the images are amenable, we can preserve the relative sizes of two |
| 63 | +# images by using either the *width_ratio* or *height_ratio* of the subplots. Which |
| 64 | +# one you use depends on the shape of the image and the size of the figure. |
| 65 | +# We can control the relative sizes using the *width_ratios* argument *if* the images |
| 66 | +# are wider than they are tall and shown side by side, as is the case here. |
| 67 | + |
| 68 | +fig, axs = plt.subplots(1, 2, width_ratios=[300/200, 1], facecolor='aliceblue') |
| 69 | + |
| 70 | +axs[0].imshow(A, vmin=-1, vmax=1) |
| 71 | +annotate_rect(axs[0]) |
| 72 | + |
| 73 | +axs[1].imshow(B, vmin=-1, vmax=1) |
| 74 | + |
| 75 | +# %% |
| 76 | +# Given that the data subsample is in the upper left of the larger image, |
| 77 | +# it might make sense if the top of the smaller Axes aligned with the top of the larger. |
| 78 | +# This can be done manually by using `~.Axes.set_anchor`, and using "NW" (for |
| 79 | +# northwest). |
| 80 | + |
| 81 | +fig, axs = plt.subplots(1, 2, width_ratios=[300/200, 1], facecolor='aliceblue') |
| 82 | + |
| 83 | +axs[0].imshow(A, vmin=-1, vmax=1) |
| 84 | +annotate_rect(axs[0]) |
| 85 | + |
| 86 | +axs[0].set_anchor('NW') |
| 87 | +axs[1].imshow(B, vmin=-1, vmax=1) |
| 88 | +axs[1].set_anchor('NW') |
| 89 | + |
| 90 | +# %% |
| 91 | +# Note that this procedure still leaves large white spaces (that can be trimmed |
| 92 | +# in a final product by ``bbox_inches="tight"`` in `~.Figure.savefig`), and is |
| 93 | +# not very general. For instance, if the axes had been arranged vertically |
| 94 | +# instead of horizontally, setting the height aspect ratio would not have |
| 95 | +# helped because the axes are wider than they are tall. For more complicated |
| 96 | +# situations it is necessary to place the axes manually. |
| 97 | +# |
| 98 | +# Manual placement |
| 99 | +# ================ |
| 100 | +# |
| 101 | +# We can manually place axes when they are created by passing a position to |
| 102 | +# `~.Figure.add_axes`. This position takes the form ``[left bottom width height]`` and |
| 103 | +# is in units that are a fraction of the figure width and height. Here we decide how |
| 104 | +# large to make the axes based on the size of the images, and add a small buffer of |
| 105 | +# 0.35 inches. We do all this at 100 dpi. |
| 106 | + |
| 107 | +dpi = 100 # 100 pixels is one inch |
| 108 | + |
| 109 | +# All variables from here are in pixels: |
| 110 | +buffer = 0.35 * dpi # pixels |
| 111 | + |
| 112 | +# Get the position of A axes |
| 113 | +left = buffer |
| 114 | +bottom = buffer |
| 115 | +ny, nx = np.shape(A) |
| 116 | +posA = [left, bottom, nx, ny] |
| 117 | +# we know this is tallest, so we can already get the fig height (in pixels) |
| 118 | +fig_height = bottom + ny + buffer |
| 119 | + |
| 120 | +# place the B axes to the right of the A axes |
| 121 | +left = left + nx + buffer |
| 122 | + |
| 123 | +ny, nx = np.shape(B) |
| 124 | +# align the bottom so that the top lines up with the top of the A axes: |
| 125 | +bottom = fig_height - buffer - ny |
| 126 | +posB = [left, bottom, nx, ny] |
| 127 | + |
| 128 | +# now we can get the fig width (in pixels) |
| 129 | +fig_width = left + nx + buffer |
| 130 | + |
| 131 | +# figsize must be in inches: |
| 132 | +fig = plt.figure(figsize=(fig_width / dpi, fig_height / dpi), facecolor='aliceblue') |
| 133 | + |
| 134 | +# the position posA must be normalized by the figure width and height: |
| 135 | +ax = fig.add_axes([posA[0] / fig_width, posA[1] / fig_height, |
| 136 | + posA[2] / fig_width, posA[3] / fig_height]) |
| 137 | +ax.imshow(A, vmin=-1, vmax=1) |
| 138 | +annotate_rect(ax) |
| 139 | + |
| 140 | +ax = fig.add_axes([posB[0] / fig_width, posB[1] / fig_height, |
| 141 | + posB[2] / fig_width, posB[3] / fig_height]) |
| 142 | +ax.imshow(B, vmin=-1, vmax=1) |
| 143 | + |
| 144 | +# %% |
| 145 | +# Inspection of the image will show that it is exactly 3* 35 + 300 + 200 = 605 |
| 146 | +# pixels wide, and 2 * 35 + 100 = 170 pixels high (or twice that if the 2x |
| 147 | +# version is used by the browser instead). The images should be rendered with |
| 148 | +# exactly 1 pixel per data point (or four, if 2x). |
| 149 | +# |
| 150 | +# .. admonition:: References |
| 151 | +# |
| 152 | +# The use of the following functions, methods, classes and modules is shown |
| 153 | +# in this example: |
| 154 | +# |
| 155 | +# - `matplotlib.axes.Axes.imshow` |
| 156 | +# - `matplotlib.figure.Figure.add_axes` |
| 157 | +# |
| 158 | +# .. tags:: |
| 159 | +# |
| 160 | +# component: figure |
| 161 | +# component: axes |
| 162 | +# styling: position |
| 163 | +# plot-type: image |
0 commit comments