1

I have a pyplot graph where each of the data points is labeled with an arrow pointing at the data point, and I have it formatted so that the data is at a constant offset from the data point it is annotating. This works fine until I get too close to the edge of figure with one of my data points and the annotation is chopped off. I am hoping that there is a way to format my annotation so that it automatically positions its self off of my data and yet stays in my figure. Below is a snippet of how I am formatting my annotation.

for label, x, y in zip(bagOlabels, time, height):
    ax.annotate(
                label,
                xy = (x,y), xytext = (50,-20),
                textcoords = 'offset points', ha = 'right', va = 'top',
                arrowprops = dict(arrowstyle = '->', 
                connectionstyle = 'arc3,rad=0')
                )

The xytext = (50,-20) is where I set the label offset. I have been doing digging, but I haven't found what I am looking for yet. If you have any insights into how this could be accomplished I would love to hear it.

Thanks.

deadstump
  • 955
  • 2
  • 11
  • 23

2 Answers2

2

There are some strategies that you can try:

  1. If the point is in the right half of the plot, put the annotation with negative offset:

    for label, x, y in zip(bagOlabels, time, height):
        offset = (50, 20)
        if x > mid_x:
           offset = (-50, 20)  # (-50, -20) could be better
    
    ax.annotate(...)
    
  2. Enlarge the plot so all the annotations fit in the plot.

It is possible that in the first case, the annotations overlap each other.

UPDATE:

The method get_xlim returns the limits of your plot in the x-axis:

x = range(0, 100)
y = map(lambda x: x*x, x)

fig = pyplot.figure()
ax1 = fig.add_subplot(1, 1, 1)
ax1.plot(x, y, 'r.', x, y, 'r-')
xlim = ax1.get_xlim()
Pablo Navarro
  • 8,244
  • 2
  • 43
  • 52
  • A conditional statement in annotate, just could work. Do you know of any way to know the limits of the axis (without explicitly setting them), so I could know when to trigger this? – deadstump Aug 10 '12 at 21:09
  • @deadstump Just updated the answer to show how to get the `xlim` of a figure. – Pablo Navarro Aug 10 '12 at 21:30
0

I found a way to do it, although crudely. It involves drawing the annotation in the normal position first, redrawing the canvas, and then if it's outside of the axis bounds, deleting it and drawing a new annotation with an updated position.

I couldn't figure out how to update the position of the original annotation. That would require the same answer as this question.

The canvas redraw fig.canvas.draw() is required to force the annotation to update it's bbox, so it can be compared to the axis bounds. I couldn't find a way yet to update the bbox without a full canvas redraw. (This requires more digging in the code)

The canvas redraw and recreating the annotation in the new position causes a visible blink on an interactive figure, but should work correctly when saving to file, where only the final position would be visible. (I have only tested it with an interactive figure)

Example PSEUDOCODE: (key pieces copied from working code)

pos_dc = (X_DC, Y_DC) # position where the arrow is pointing, in data coordinates

xytext = [20, 20] # normal x and y position of the annotation textbox, in points coordines
xytext_flip = [-80, -80] # flipped x and y positions, when it's too close to edge, in points coordines
TEXT = ("Text box contents" + "\n" +
        "with multiple" + "\n" +
        "lines"
        )

# create original annotation to measure its position
annot = ax.annotate ("", xy=(0,0), xytext=(xytext[0],xytext[1]),
            textcoords="offset points", bbox=dict(boxstyle="round",
            fc="w"), arrowprops=dict(arrowstyle="->", color='black')
        )
annot.xy = pos_dc
annot.set_text (TEXT)

# draw the canvas, forcing the annotation position to be updated
# fig.canvas.draw_idle() didn't work here
fig.canvas.draw()

# measure the annotation's position
p = annot.get_bbox_patch()
ex = p.get_extents() # annotation textbox, in pixel coordinates
T = ax.transData.inverted()
ex_datac = T.transform (ex) # annotation textbox, in data coordinates

# ex_datac structure in data coordinates:
# [[xleft  ybottom] [xright  ytop]]

ax_xb = ax.get_xbound()
ax_yb = ax.get_ybound() # axis bounds (the visible area), in data coordinates

# structure in data coordinates:
# ax_xb = (xleft, xright) , ax_yb = (ybottom, ytop)

# in data coordinates, (y) top is a GREATER value than bottom, use '>' to compare it
dc_top_of_display = ax_yb[1]
dc_bottom_of_display = ax_yb[0]
dc_left_of_display = ax_xb[0]
dc_right_of_display = ax_xb[1]

# only testing the right and top edges of the annotation textbox, which was sufficient in this case
dc_box_right = ex_datac[1][0]
dc_box_top = ex_datac[1][1]

# test whether the annotation textbox is outside of the axis bounds
flip_x = dc_box_right > dc_right_of_display
flip_y = dc_box_top > dc_top_of_display # top is GREATER than bottom, use '>'

# if the text box right or top edges are outside of the axis bounds, update the annotation's position
if flip_x or flip_y:
    xytext2 = [0, 0]
    xytext2[0] = xytext[0] if not flip_x else xytext_flip[0]
    xytext2[1] = xytext[1] if not flip_y else xytext_flip[1]

    # remove the original annotation
    annot.remove ()

    # create new annotation with updated position
    annot = ax.annotate ("", xy=(0,0), xytext=(xytext2[0],xytext2[1]),
                textcoords="offset points", bbox=dict(boxstyle="round",
                fc="w"), arrowprops=dict(arrowstyle="->", color='black')
            )
    annot.xy = pos_dc
    annot.set_text (TEXT)

    # redraw the canvas
    fig.canvas.draw_idle()
gregn3
  • 1,728
  • 2
  • 19
  • 27