5

for a certain manuscript i need to position my label of the Graph exactly in the right or left top corner. The label needs a border with the same thickness as the spines of the graph. Currently i do it like this:

import matplotlib.pyplot as plt
import numpy as np
my_dpi=96
xposr_box=0.975 
ypos_box=0.94
nrows=3
Mytext="label"
GLOBAL_LINEWIDTH=2
fig, axes = plt.subplots(nrows=nrows, sharex=True, sharey=True, figsize=
               (380/my_dpi, 400/my_dpi), dpi=my_dpi)
fig.subplots_adjust(hspace=0.0001)
colors = ('k', 'r', 'b')
for ax, color in zip(axes, colors):
    data = np.random.random(1) * np.random.random(10)
    ax.plot(data, marker='o', linestyle='none', color=color)

for ax in ['top','bottom','left','right']:
    for idata in range(0,nrows):
        axes[idata].spines[ax].set_linewidth(GLOBAL_LINEWIDTH)


axes[0].text(xposr_box, ypos_box , Mytext, color='black',fontsize=8,
             horizontalalignment='right',verticalalignment='top', transform=axes[0].transAxes,
             bbox=dict(facecolor='white', edgecolor='black',linewidth=GLOBAL_LINEWIDTH)) 

plt.savefig("Label_test.png",format='png', dpi=600,transparent=True)

Image1

So i control the position of the box with the parameters:

xposr_box=0.975 
ypos_box=0.94

If i change the width of my plot, the position of my box also changes, but it should always have the top and right ( or left) spine directly on top of the graphs spines:

import matplotlib.pyplot as plt
import numpy as np
my_dpi=96
xposr_box=0.975 
ypos_box=0.94
nrows=3
Mytext="label"
GLOBAL_LINEWIDTH=2
fig, axes = plt.subplots(nrows=nrows, sharex=True, sharey=True, figsize=
               (500/my_dpi, 400/my_dpi), dpi=my_dpi)
fig.subplots_adjust(hspace=0.0001)
colors = ('k', 'r', 'b')
for ax, color in zip(axes, colors):
    data = np.random.random(1) * np.random.random(10)
    ax.plot(data, marker='o', linestyle='none', color=color)

for ax in ['top','bottom','left','right']:
    for idata in range(0,nrows):
        axes[idata].spines[ax].set_linewidth(GLOBAL_LINEWIDTH)


axes[0].text(xposr_box, ypos_box , Mytext, color='black',fontsize=8,
             horizontalalignment='right',verticalalignment='top',transform=axes[0].transAxes,
             bbox=dict(facecolor='white', edgecolor='black',linewidth=GLOBAL_LINEWIDTH)) 

plt.savefig("Label_test.png",format='png', dpi=600,transparent=True)

Image2

This should also be the case if the image is narrower not wider as in this example.I would like to avoid doing this manually. Is there a way to always position it where it should? Independent on the width and height of the plot and the amount of stacked Graphs?

Bless
  • 5,052
  • 2
  • 40
  • 44
NorrinRadd
  • 545
  • 6
  • 21

2 Answers2

4

The problem is that the position of a text element is relative to the text's extent, not to its surrounding box. While it would in principle be possible to calculate the border padding and position the text such that it sits at coordinates (1,1)-borderpadding, this is rather cumbersome since (1,1) is in axes coordinates and borderpadding in figure points.

There is however a nice alternative, using matplotlib.offsetbox.AnchoredText. This creates a textbox which can be placed easily relative the the axes, using the location parameters like a legend, e.g. loc="upper right". Using a zero padding around that text box directly places it on top of the axes spines.

from matplotlib.offsetbox import AnchoredText
txt = AnchoredText("text", loc="upper right", pad=0.4, borderpad=0, )
ax.add_artist(txt)

A complete example:

import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnchoredText
import numpy as np

my_dpi=96
nrows=3
Mytext="label"
plt.rcParams["axes.linewidth"] = 2
plt.rcParams["patch.linewidth"] = 2

fig, axes = plt.subplots(nrows=nrows, sharex=True, sharey=True, figsize=
               (500./my_dpi, 400./my_dpi), dpi=my_dpi)
fig.subplots_adjust(hspace=0.0001)
colors = ('k', 'r', 'b')
for ax, color in zip(axes, colors):
    data = np.random.random(1) * np.random.random(10)
    ax.plot(data, marker='o', linestyle='none', color=color)

txt = AnchoredText(Mytext, loc="upper right", 
                   pad=0.4, borderpad=0, prop={"fontsize":8})
axes[0].add_artist(txt)

plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Can you pass coordinates to `loc`, or can it only be aligned to the edges of the `Axes`? The documentation is not very verbose about this, it only says `str` and `location code`. – Thomas Kühn Feb 07 '18 at 13:33
  • @ThomasKühn You can pass a tuple of coordinates, `loc=(1,1)`, but that would put the lower left corner of the box at that position. However, the mechanism is exactly the same as for a legend, see e.g. [this answer](https://stackoverflow.com/a/43439132/4124317), or the answers to [this question](https://stackoverflow.com/questions/39803385/what-does-a-4-element-tuple-argument-for-bbox-to-anchor-mean-in-matplotlib). Essentially you'd use `bbox_to_anchor` to specify the coordinates and `loc` to set the alignment inside this bbox. – ImportanceOfBeingErnest Feb 07 '18 at 13:46
  • E.g. `AnchoredText(loc="center", bbox_to_anchor=(.7,.7))` to put the text box centered at position `(.7,.7)` in axes coordinates. There is also a `bbox_transform` argument which can be used to supply a transform to use. – ImportanceOfBeingErnest Feb 07 '18 at 13:46
  • Thank you once again!!!! This worked fine! I had however and error which was resolved after i used the integers instead of the strings for the `loc` tag of `AnchoredText`. Specifically : `'upper right' : 1`, and `'upper left' : 2`. – NorrinRadd Feb 07 '18 at 21:47
1

In principle you can align text to the Axes spines using Annotations and position them in Axes coordinates (x and y between 0 and 1) using xycoords = 'axes fraction. However, because you use a bbox that bbox will overlap with the spines.

Instead, you can use ax.text together a ScaledTransformation, which, if done right, also positions the text in Axes coordinates and shifts it by a fixed amount. If you provide a pad size to the bbox keyword, you know exactly how much the bbox will overlap with the spines in figure points (1 inch is 72 points), so that the shift is easily calculated. Here a little demonstration how to do this:

from matplotlib import pyplot as plt
import numpy as np
import matplotlib.transforms as transforms

GLOBAL_LINEWIDTH=2
pad = 10

fig,ax = plt.subplots()
x = np.linspace(0,1,20)

ax.plot(x,x**2, 'ro')

offset_bl = transforms.ScaledTranslation(
    pad/72, pad/72, fig.dpi_scale_trans,
)
offset_br = transforms.ScaledTranslation(
    -pad/72, pad/72, fig.dpi_scale_trans,
)
offset_tl = transforms.ScaledTranslation(
    pad/72, -pad/72, fig.dpi_scale_trans,
)
offset_tr = transforms.ScaledTranslation(
    -pad/72, -pad/72, fig.dpi_scale_trans,
)

for pos in ['top','bottom','left','right']:
    ax.spines[pos].set_linewidth(GLOBAL_LINEWIDTH)

ax.text(
    0,0, 'bottom left', 
    fontsize = 16, fontweight='bold', va='bottom', ha='left',
    bbox=dict(
        facecolor = 'white', edgecolor='black', lw = GLOBAL_LINEWIDTH,
        pad = pad
    ),
    transform=ax.transAxes + offset_bl,
)    

ax.text(
    1,0, 'bottom right', 
    fontsize = 16, fontweight='bold', va='bottom', ha='right',
    bbox=dict(
        facecolor = 'white', edgecolor='black', lw = GLOBAL_LINEWIDTH,
        pad = pad
    ),
    transform=ax.transAxes + offset_br,
)    

ax.text(
    0,1, 'top left', 
    fontsize = 16, fontweight='bold', va='top', ha='left',
    bbox=dict(
        facecolor = 'white', edgecolor='black', lw = GLOBAL_LINEWIDTH,
        pad = pad
    ),
    transform=ax.transAxes + offset_tl,
)    

ax.text(
    1,1, 'top right', 
    fontsize = 16, fontweight='bold', va='top', ha='right',
    bbox=dict(
        facecolor = 'white', edgecolor='black', lw = GLOBAL_LINEWIDTH,
        pad = pad
    ),
    transform=ax.transAxes + offset_tr,
)    

plt.show()

And here is the result:

result of the above code

Thomas Kühn
  • 9,412
  • 3
  • 47
  • 63
  • I guess this would also work, but i went with the answer above! Thank you for taking the time to look at my problem! – NorrinRadd Feb 07 '18 at 21:48