7

I have produced 2 plots in Matplotlib with Python 2.7. The plots were saved to *.png files. After saving them, both images have the same resolution - width = 1099 pixels, height = 619 pixels.

However, when I align the saved *.png images vertically (attached, below), the spacing between the y-axis and the left most point of the images is not the same - see a and b in the image below.enter image description here

By this I mean, the distance from the left of the image to the y-axis is not the same (a is not equal to b).

Click on the image to zoom in and see this.

Question: Is there a way to force the y-axis to start at a particular position relative to the left of the image?

NOTE: I am not concerned about the space between the tick label and the axis label - I can adjust this using something like ax.yaxis.labelpad(25). However, I do not know how to fix the space between the left of the image and the y-axis.

NOTE 2: I create my plot using:

fig = plt.figure(1)
ax = fig.add_subplot(111)
fig.tight_layout()
Jean-Sébastien
  • 2,649
  • 1
  • 16
  • 21
edesz
  • 11,756
  • 22
  • 75
  • 123
  • 2
    This is exactly what `tight_layout()` is supposed to adjust. Internally the artists in the `Figure` are placed/sized in a [0, 1] unit space. At render time that space is transformed to screen/pixel space. What you are seeing is a _feature_ of `tight_layout`, not a bug. If you don't want this don't use `tight_layout.` – tacaswell Aug 11 '15 at 02:43
  • Related: Changing the distance between an [axis and its corresponding label](https://stackoverflow.com/questions/6406368/matplotlib-move-x-axis-label-downwards-but-not-x-axis-ticks) – ijuneja Sep 29 '20 at 06:28

2 Answers2

7

This is how I usually setup my code if I want to have a fine control over the size of the figure's margins in matplotlib. In addition, I show how the position of the ylabel can be setup, so you can easily align the ylabels of your two figures together.

import matplotlib.pyplot as plt

plt.close('all')

#---- create figure ----

fwidth = 8.  # total width of the figure in inches
fheight = 4. # total height of the figure in inches

fig = plt.figure(figsize=(fwidth, fheight))

#---- define margins -> size in inches / figure dimension ----

left_margin  = 0.95 / fwidth
right_margin = 0.2 / fwidth
bottom_margin = 0.5 / fheight
top_margin = 0.25 / fheight

#---- create axes ----

# dimensions are calculated relative to the figure size

x = left_margin    # horiz. position of bottom-left corner
y = bottom_margin  # vert. position of bottom-left corner
w = 1 - (left_margin + right_margin) # width of axes
h = 1 - (bottom_margin + top_margin) # height of axes

ax = fig.add_axes([x, y, w, h])

#---- Define the Ylabel position ----

# Location are defined in dimension relative to the figure size  

xloc =  0.25 / fwidth 
yloc =  y + h / 2.  

ax.set_ylabel('yLabel', fontsize=16, verticalalignment='top',
              horizontalalignment='center')             
ax.yaxis.set_label_coords(xloc, yloc, transform = fig.transFigure)

plt.show(block=False)
fig.savefig('figure_margins.png')

This results in a 8in x 4in figure, with margins of exactly 0.95, 0.2, 0.5, and 0.25 inch at the left, right, bottom and top of the figure. One benefit of this approach is that the size of the margins are defined in absolute units (inches), meaning they will remain consistent even if you change the size of the figure.

As for the ylabel, horizontally, the top of the label is located 0.25 inch from the left edge of the figure, while vertically the centre of the label corresponds to the centre of the axe. Note that due to the 90 degrees rotation on the ylabel, the meaning of the verticalalignment and horizontalalignment are in reality inverted.

Below are shown outputs of the code above with the yaxis limits set to [0, 1] and to [0, 18] respectively.

enter image description here enter image description here

Jean-Sébastien
  • 2,649
  • 1
  • 16
  • 21
  • Many thanks. Your code `w = 1 - (left_margin + right_margin)` is the fundamental understanding that I was missing. Can you also show the plot when you set `ax.set_ylim([0,18.0])` for comparison? – edesz Aug 11 '15 at 02:55
  • 1
    @WR Glad it helped. Good idea for the second plot, I've added it. – Jean-Sébastien Aug 11 '15 at 03:04
  • Can you also explain your 3rd last line - this one `ax.yaxis.set_label_coords(xloc, yloc, transform = fig.transFigure)`? – edesz Aug 11 '15 at 03:08
  • 1
    @WR By default, "set_label_coords" takes (x, y) in dimension relative to the size of the axe. In your case, I thought it would be a better design if it was defined in dimension relative to the figure size instead. This is why I added the `fig.transFigure`. This allowed to setup the label position with an approach that was consistent with the rest of the code. Also, the position should remain good even if you change the size of the figure. – Jean-Sébastien Aug 11 '15 at 03:15
  • The last sentence in your last comment is likely the key for me. The line `ax.yaxis.set_label_coords()` is extremely important - I was not aware of this command and what it can do. Thank you again. – edesz Aug 11 '15 at 03:33
  • In the case of multiple subplots (eg. 2x2), how would your approach change? i.e. how would you pick the margins and `xloc,yloc`? – edesz Aug 11 '15 at 14:48
  • 1
    @WR In this case, if your 4 subplots have the same horizontal dimension, I would define the yaxis location manually in coordinate relative to the axes instead, like shown here: http://stackoverflow.com/questions/19277324/matplotlib-aligning-y-axis-labels-in-stacked-scatter-plots/19277494#19277494. It requires to adjust manually the x-position though. A more automated approach can be achieved using the `get_ticklabel_extents` method. – Jean-Sébastien Aug 11 '15 at 15:29
  • Thank you again. The suggestion from that link worked. All my questions in this post have been answered. – edesz Aug 11 '15 at 15:39
4

I think you can set this property (space between edge of figure and axis) when you create the axes (add_axes to a figure object). Here is some simple example code that produces two axes with generous spacing on all sides:

import matplotlib.pyplot as plt

f1 = plt.figure()
ax1 = f1.add_axes([0.2, 0.2, 0.6, 0.6]) # List is [left, bottom, width, height]
ax1.axis([0, 1, 0, 1])
plt.savefig('ax1.png')

f2 = plt.figure()
ax2 = f2.add_axes([0.2, 0.2, 0.6, 0.6])
ax2.axis([0, 1000, 0, 1000])
plt.savefig('ax2.png')

You can find more info about it here:
http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.add_axes

Edit: You can achieve a similar result using subplots_adjust. Using your example code:

fig = plt.figure(1)
ax = fig.add_subplot(111)
fig.tight_layout()
plt.subplots_adjust(left=0.2, bottom=0.2, right=0.8, top=0.8)
jwinterm
  • 334
  • 3
  • 11
  • I am adding a single sub ploty using `fig = plt.figure(1)` and `ax = fig.add_subplot(111)`. Is there a way for me to build `.add_axes()` into this? – edesz Aug 10 '15 at 20:50
  • See my edits in the original post for details about how I am creating my plot. Let me know what you think. – edesz Aug 10 '15 at 21:09
  • @WR Is there a reason why you cannot or do not want to replace `ax = fig.add_subplot(111)` by `ax = fig.add_axes([x, y, w, h])` as proposed by jwinterm? Seems like a reasonable solution to control the size of the margins as you which. – Jean-Sébastien Aug 10 '15 at 21:16
  • @jwinterm: Could you explain what this `[0.2, 0.1, 1, 1]` would give? As I understand it, the y-axis would start at 20% to the right from the left edge of the figure and the x-axis would be 10% up from the bottom edge of the figure. Is this the right interpretation? Also, I am not sure what the documentation means by the last 2 values - do they just scale the width and height up and down? – edesz Aug 10 '15 at 21:50
  • 1
    Your interpretation is correct for the first part, [0.2, 0.1... does mean that it will start 20% from left and 10% from bottom, but the ...1, 1] would make the width and height of the axis 100% of the figure, effectively making part of the axis off the edge of the figure. So, [0.0, 0.0, 1.0, 1.0] would make an axis on the figure that starts at bottom left corner and occupies the whole thing, and [0.15, 0.15, 0.8, 0.8] might be more typical of the default behavior (where there is space for axis labels and such. – jwinterm Aug 10 '15 at 23:57
  • So, if I were to use [0.2,0.1,... then it would be best to go with ...,0.8,0.9]. Would this be an example of the intended usage? – edesz Aug 11 '15 at 00:19
  • @jwinterm: For the suplots_adjust version, your settings (0.2,0.8) work. But can something smaller on the left and something larger on the right be used? eg. 0.15,0.85 do not work, but I can't understand why. Is there a reason for this? – edesz Aug 11 '15 at 01:33