0

I am searching for an elegant solution to placing a figure label (A, B, C) above the corner of each subplot using matplotlib- but have each be the exact same distance from the subplot axes corner. The problem I am having is that typical labeling solutions take advantage of the axes fraction- so it is easy to place A,B,C in the same place relative to each plot.

import matplotlib as mpl
import matplotlib.pyplot as plt
fig, ax = plt.subplots(2,2, figsize = (10,10))

texts = ['A', 'B', 'C', 'D']
axes = fig.get_axes()
for a,l in zip(axes, texts):
    a.annotate(l, xy=(-0.1, 1.1), xycoords="axes fraction", fontsize=15, weight = 'bold')
plt.suptitle('Label_Distance Consistent', fontsize = 20)

enter image description here

however, if the plots are different sizes you will get labels that are variably far from the corners of the plots (due to aspect ratio). See Label A and C for example. I am looking for a good way to ensure proportional distance of labels from axes corners for panels containing multiple sizes/aspect ratio subplots, and/or to explicitly set text a specific distance (in inches or maybe figure coordinate units) from axes corners.

In the past I have placed same sized square axes at the corner of each subplot axes in the panel, made those invisible, and scaled text to these, but it is a convoluted approach.

fig, ax = plt.subplots(2,2, figsize = (10,10))

ax1 = plt.subplot2grid((3, 3), (0, 0), colspan=3)
ax2 = plt.subplot2grid((3, 3), (1, 0), colspan=2)
ax3 = plt.subplot2grid((3, 3), (1, 2), rowspan=2)
ax4 = plt.subplot2grid((3, 3), (2, 0))
ax5 = plt.subplot2grid((3, 3), (2, 1))

axes = fig.get_axes()
texts = ['A', 'B', 'C', 'D', 'E']
for a,l in zip(axes, texts):
    a.annotate(l, xy=(-0.1, 1.1), xycoords="axes fraction", fontsize=15, weight = 'bold')
plt.suptitle('Label Distance Inconsistent', fontsize = 20)

enter image description here

djakubosky
  • 907
  • 8
  • 15

4 Answers4

3

You could use axes pixels istead of axes fraction as reference and add the individual heights to compensate the reference origin lower left corner:

Here for brevity only the changed line within your loop, rest of your code untouched:

    a.annotate(l, xy=(-40, 10 + a.bbox.height), xycoords="axes pixels", fontsize=15, weight = 'bold')

enter image description here

SpghttCd
  • 10,510
  • 2
  • 20
  • 25
  • Great answer, this almost does what I was hoping. I am finding that this doesn't work consistently when the dpi is not 80, is there a way to account for this? So if I scale up my dpi to 100 for example, labels will not be consistent from axes again- C will be much further (height wise) from the corner of ax3 (bottom right). Is there a way perhaps to set the text a specified distance in inches (x inches, y inches) in relation to axes top corners so that this could be robust to dpi changes/figure dimensions? Thanks so much for the help! – djakubosky Sep 12 '18 at 19:17
  • On second thought, this doesn't seem to quite work at any DPI I try, it could be matplotlib version specific too because your image looks consistent (I am 2.2.3). It seems that a.bbox.height doesn't return a value that is true to the heights of the plots for me. – djakubosky Sep 12 '18 at 19:34
  • Just tested it with matplotlib 2.2.3 and it still works with `a.bbox.height` and `axes pixels`. – SpghttCd Sep 13 '18 at 08:58
  • it definitely makes sense that it would work, but for whatever reason, my lettering still scales with the dimensions of the plots. In fact, even if I plot them with xy= (0,bbox.height) (theoretically top corner) the letters aren't placed in the corners. strange. I have observed other strange results in this ax.annotate method, namely that plotting on xycoords='figure points' or 'figure pixels' doesn't seem to place text where its expected- this is why in my answer below I converted from display back to axes fraction. – djakubosky Sep 13 '18 at 19:49
  • Confirming this works for others I work with, must be something specific to my software config. – djakubosky Sep 13 '18 at 20:13
1

What about simply adding the title of the subplots?

(Again only the line to be replaced within your loop:)

a.set_title(l, size=15, weight='bold', loc='left')

Result: enter image description here

SpghttCd
  • 10,510
  • 2
  • 20
  • 25
1

You can get the position of each subplot and add text on figure-level, so that offsets are in fractions of the figure size - which is equal for all subplots:

X = a.get_position().x0
Y = a.get_position().y1    
fig.text(X - .04, Y + .015, l, size=15, weight='bold')

Result: enter image description here

SpghttCd
  • 10,510
  • 2
  • 20
  • 25
  • accepting this answer as it solves most requirements of my question and will probably serve most peoples needs. – djakubosky Sep 13 '18 at 19:39
0

While both of the above solutions work (and are more simple/meet most peoples needs), I thought I'd post a solution I pieced together allowing you to place text/objects with exact inches/points of offset from a particular axes point. This means that regardless of figure panel size/dpi, the text could be placed the same distance from corners- useful if making multiple figures of different dimensions and want them to be consistent (eg for publication). Apparently the matplotlib.transforms.offset_copy() function was designed for this purpose allowing specification of inches, points (1/72 inch) or dots as offset.

https://matplotlib.org/examples/pylab_examples/transoffset.html

import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

fig, ax = plt.subplots(2,2, figsize = (10,7), dpi = 500)

ax1 = plt.subplot2grid((3, 3), (0, 0), colspan=3)
ax2 = plt.subplot2grid((3, 3), (1, 0), colspan=2)
ax3 = plt.subplot2grid((3, 3), (1, 2), rowspan=2)
ax4 = plt.subplot2grid((3, 3), (2, 0))
ax5 = plt.subplot2grid((3, 3), (2, 1))

axes = fig.get_axes()
texts = ['A', 'B', 'C', 'D', 'E']
for a,l in zip(axes, texts):
    a.set_xlim(0,10)
    inv = a.transData.inverted()

    # specify the number of points or inches or dots to transform by
    # places an axes transform here for the axes on which you want to pick an anchor point on
    offsetter = mpl.transforms.offset_copy(a.transAxes, fig=fig, x= -2, y= 6, units = 'points')

    # offset an anchor point - this will return display coordinates 
    corner_offset = offsetter.transform([0,1])

    # convert display coordinate to axes fraction
    axes_frac = inv.transform(corner_offset)
    a.annotate(l, xy=(axes_frac[0],axes_frac[1]), xycoords="axes fraction", fontsize=15, weight = 'bold', color='blue')

enter image description here

or with figsize/dpi changed

enter image description here

djakubosky
  • 907
  • 8
  • 15