Thanks to visit codestin.com
Credit goes to github.com

Skip to content

DOC: manually placing images example #28775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 5, 2024
Merged

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Sep 2, 2024

New example showing a couple of methods for dealing with images that you want to keep the same reelative size.

@github-actions github-actions bot added the Documentation: examples files in galleries/examples label Sep 2, 2024
@jklymak jklymak marked this pull request as ready for review September 2, 2024 02:42
Copy link
Member

@rcomer rcomer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems good to me (and also familiar… 😉). Just a few minor comments.

@jklymak
Copy link
Member Author

jklymak commented Sep 2, 2024

Thanks @rcomer - yes this came from the latest stack overflow to ask for this. It is asked periodically, and I did not see that we had a good example to point to.

Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to add # sphinx_gallery_thumbnail_number = -1 so that the last image will be used as thumbnail.

@jklymak
Copy link
Member Author

jklymak commented Sep 2, 2024

I suggest to add # sphinx_gallery_thumbnail_number = -1 so that the last image will be used as thumbnail.

Yes, good idea - I also toyed with the idea of making the default figure size more appropriate, but I think it's reasonable to use the default so folks can see how it can "go wrong".

Comment on lines 100 to 146
dpi = 100 # 100 pixels is one inch
buffer = 0.35 * dpi # pixels
left = buffer
bottom = buffer
ny, nx = np.shape(A)
posA = [left, bottom, nx, ny]
# we know this is tallest, so we can get the fig height:
figh = bottom + ny + buffer

# place the B axes
left = left + nx + buffer
ny, nx = np.shape(B)
posB = [left, figh - buffer - ny, nx, ny]

# get the fig width
figw = left + nx + buffer

fig = plt.figure(figsize=(figw / dpi, figh / dpi), facecolor='aliceblue')

ax = fig.add_axes([posA[0] / figw, posA[1] / figh, posA[2] / figw, posA[3] / figh])
ax.imshow(A, vmin=-1, vmax=1)
rect = mpatches.Rectangle((0, 0), 200, 40, linewidth=1, edgecolor='r', facecolor='none')
ax.add_patch(rect)

ax = fig.add_axes([posB[0] / figw, posB[1] / figh, posB[2] / figw, posB[3] / figh])
ax.imshow(B, vmin=-1, vmax=1)
Copy link
Member

@timhoffm timhoffm Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion structuring to make the calculations more understandable:

  1. Calculate desired figure height and width in pixel
  2. Create figure (needs calculation of figure size in inches)
  3. Add axes (needs calculation of dimensions in figure coordinates)
Suggested change
dpi = 100 # 100 pixels is one inch
buffer = 0.35 * dpi # pixels
left = buffer
bottom = buffer
ny, nx = np.shape(A)
posA = [left, bottom, nx, ny]
# we know this is tallest, so we can get the fig height:
figh = bottom + ny + buffer
# place the B axes
left = left + nx + buffer
ny, nx = np.shape(B)
posB = [left, figh - buffer - ny, nx, ny]
# get the fig width
figw = left + nx + buffer
fig = plt.figure(figsize=(figw / dpi, figh / dpi), facecolor='aliceblue')
ax = fig.add_axes([posA[0] / figw, posA[1] / figh, posA[2] / figw, posA[3] / figh])
ax.imshow(A, vmin=-1, vmax=1)
rect = mpatches.Rectangle((0, 0), 200, 40, linewidth=1, edgecolor='r', facecolor='none')
ax.add_patch(rect)
ax = fig.add_axes([posB[0] / figw, posB[1] / figh, posB[2] / figw, posB[3] / figh])
ax.imshow(B, vmin=-1, vmax=1)
margin_px = 35 # in pixels
ny_A, nx_A = np.shape(A)
ny_B, nx_B = np.shape(B)
fig_height_px = max(ny_A, ny_B) + 2 * margin_px
fig_width_px = nx_A + nx_B + 2 * margin_px
# calculate figsize in inches
figsize = (fig_width_px / dpi, fig_height_px / dpi)
fig = plt.figure(figsize=figsize, facecolor='aliceblue')
# add axes in figure coordinates
ax1 = fig.add_axes([
margin_px / fig_width_px, # left
margin_px / fig_height_px, # bottom
nx_A / fig_width_px, # width
ny_A / fig_height_px # height
])
ax2 = fig.add_axes([
margin_px / fig_width_px, # left
(fig_height_px - margin_px - ny) / fig_height_px, # bottom
nx_B / fig_width_px, # width
ny_B / fig_height_px # height
])
ax1.imshow(A, vmin=-1, vmax=1)
rect = mpatches.Rectangle((0, 0), 200, 40, linewidth=1, edgecolor='r', facecolor='none')
ax.add_patch(rect)
ax2.imshow(B, vmin=-1, vmax=1)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A and B are purposefully different widths, so I don't think this is correct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. The numbers / expressions are not correct. Still the idea (which possibly can be communicated more explicitly) is that you can place everything at exact pixel positions. And to do that, you have to follow the three steps above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly now correct. - I don't have an interpreter at hand, so can't check, but I hope you get the idea.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do get the idea, but not convinced pre-calculating the figure dimensions buys you anything. Also, imagine we were to add a third or fourth image in there, we would need to change the code in two places.

Just for context, I originally wrote as a for-loop to allow flexibility, but backed off on that as perhaps too magical.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for context, I originally wrote as a for-loop to allow flexibility, but backed off on that as perhaps too magical.

👍 Examples must show the pattern, not necessarily a generic solution if that makes the code harder to understand.


I had difficulty following your code. Reflecting on it, the reasons were

  1. lines 100-115 define a lot of variables, which are sometimes
    a) tersely named (figh)
    b) cryptic data structures posB = [left, figh - buffer - ny, nx, ny]
    c) a mixture of definitions for later use (e.g. figh) and temporary local variables that are reassigned (left, nx, `ny´)
  2. It's unclear that these are all in pixels
  3. Transformation calculations to inches (for figsize) and figure units (for add_axes) are hard to understand, because both the input units (see 2.) and the resulting units are not clear.

I tried a more minimal refactor of your code to alleviate these points, but I ended up with almost the same code as I've proposed above 🤷‍♂️.

@jklymak
Copy link
Member Author

jklymak commented Sep 3, 2024

The doc build is failing with

File "/home/circleci/project/galleries/examples/units/radian_demo.py", line 26, in <module>
        axs[0].plot(x, cos(x), xunits=radians)
                       ^^^^^^
      File "/home/circleci/project/galleries/examples/units/basic_units.py", line 382, in cos
        return [math.cos(val.convert_to(radians).get_value()) for val in x]
                         ^^^^^^^^^^^^^^
    AttributeError: 'numpy.float64' object has no attribute 'convert_to'

@timhoffm
Copy link
Member

timhoffm commented Sep 3, 2024

Please rebase. #28779 fixes the doc build.

@timhoffm
Copy link
Member

timhoffm commented Sep 4, 2024

I also toyed with the idea of making the default figure size more appropriate, but I think it's reasonable to use the default so folks can see how it can "go wrong".

I'm +1 on adjusting the figure size right away for all figures. Adjusting the figsize is something one would always do if the default aspect is not a good fit for the content. This is something we just do as needed without talking about it (example: https://matplotlib.org/stable/gallery/lines_bars_and_markers/categorical_variables.html).

Just looking at the figures, one could get the impression, that it's a feature of the "manual" approach that you don't have the large figure whitespace. To take this effect out, all figures should have the same (or similar) size.

@jklymak
Copy link
Member Author

jklymak commented Sep 4, 2024

I'm not sure I agree with that. Of course people should change the figure size, but there is endless confusion about fixed ratio images and why there is so much white space. If we show the white space, I think on the balance we are helping folks understand this is not something matplotlib will do for them automagically.

# helped because the axes are wider than they are tall. For more complicated
# situations it is necessary to place the axes manually.
#
# Manual placement
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit torn on the motivation for this: You can get away without by using an appropriate figsize (which is much simpler).

The advantages I see is

  1. pixel-perfect positioning (if you need that)
  2. in this case 1:1 mapping from data points to visual pixels.

Maybe these should be highlighted more.

@jklymak
Copy link
Member Author

jklymak commented Sep 6, 2024

@timhoffm perhaps this is worse, but this change I don't set the figsize for the first "default" plot, but then do set it for the customized plots, with a bit of explanation. That doesn't make the narrative shorter, but it does go through the steps in the way a user may go through them.

https://output.circle-artifacts.com/output/job/d67643fd-551a-4486-8c8a-409ddbdef5ba/artifacts/0/doc/build/html/gallery/images_contours_and_fields/image_exact_placement.html#sphx-glr-gallery-images-contours-and-fields-image-exact-placement-py

@timhoffm
Copy link
Member

timhoffm commented Sep 7, 2024

I like this. It's clear without making too much fuss about the figsize topic.

Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take or leave the comments. May self-merge afterwards.

Comment on lines +7 to +8
fit inside the parent `~.axes.Axes`. This can mean that images that have very
different original sizes can end up appearing similar in size.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Slightly more concise wording:

Suggested change
fit inside the parent `~.axes.Axes`. This can mean that images that have very
different original sizes can end up appearing similar in size.
fit inside the parent `~.axes.Axes`. As a result, images with very different
original sizes may end up appearing similar in size.

@jklymak jklymak merged commit 6119c12 into matplotlib:main Dec 5, 2024
22 checks passed
@QuLogic QuLogic added this to the v3.11.0 milestone Dec 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Documentation: examples files in galleries/examples
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants