Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to rotate matplotlib annotation to match a line?

Tags:

Have a plot with several diagonal lines with different slopes. I would like to annotate these lines with a text label that matches the slope of the lines.

Something like this:

Annotated line

Is there a robust way to do this?

I've tried both text's and annotate's rotation parameters, but those are in screen coordinates, not data coordinates (i.e. it's always x degrees on the screen no matter the xy ranges). My x and y ranges differ by orders of magnitude, and obviously the apparent slope is affected by viewport size among other variables, so a fixed-degree rotation doesn't do the trick. Any other ideas?

like image 711
Adam Avatar asked Sep 13 '13 07:09

Adam


People also ask

How do I rotate in MatPlotLib?

Rotate X-Axis Tick Labels in Matplotlib There are two ways to go about it - change it on the Figure-level using plt. xticks() or change it on an Axes-level by using tick. set_rotation() individually, or even by using ax.

How do I annotate in MatPlotLib?

The annotate() function in pyplot module of matplotlib library is used to annotate the point xy with text s. Parameters: This method accept the following parameters that are described below: s: This parameter is the text of the annotation. xy: This parameter is the point (x, y) to annotate.


1 Answers

I came up with something that works for me. Note the grey dashed lines:

annotated lines

The rotation must be set manually, but this must be done AFTER draw() or layout. So my solution is to associate lines with annotations, then iterate through them and do this:

  1. get line's data transform (i.e. goes from data coordinates to display coordinates)
  2. transform two points along the line to display coordinates
  3. find slope of displayed line
  4. set text rotation to match this slope

This isn't perfect, because matplotlib's handling of rotated text is all wrong. It aligns by the bounding box and not by the text baseline.

Some font basics if you're interested about text rendering: http://docs.oracle.com/javase/tutorial/2d/text/fontconcepts.html

This example shows what matplotlib does: http://matplotlib.org/examples/pylab_examples/text_rotation.html

The only way I found to have a label properly next to the line is to align by center in both vertical and horizontal. I then offset the label by 10 points to the left to make it not overlap. Good enough for my application.

Here is my code. I draw the line however I want, then draw the annotation, then bind them with a helper function:

line, = fig.plot(xdata, ydata, '--', color=color)  # x,y appear on the midpoint of the line  t = fig.annotate("text", xy=(x, y), xytext=(-10, 0), textcoords='offset points', horizontalalignment='left', verticalalignment='bottom', color=color) text_slope_match_line(t, x, y, line) 

Then call another helper function after layout but before savefig (For interactive images I think you'll have to register for draw events and call update_text_slopes in the handler)

plt.tight_layout() update_text_slopes() 

The helpers:

rotated_labels = [] def text_slope_match_line(text, x, y, line):     global rotated_labels      # find the slope     xdata, ydata = line.get_data()      x1 = xdata[0]     x2 = xdata[-1]     y1 = ydata[0]     y2 = ydata[-1]      rotated_labels.append({"text":text, "line":line, "p1":numpy.array((x1, y1)), "p2":numpy.array((x2, y2))})  def update_text_slopes():     global rotated_labels      for label in rotated_labels:         # slope_degrees is in data coordinates, the text() and annotate() functions need it in screen coordinates         text, line = label["text"], label["line"]         p1, p2 = label["p1"], label["p2"]          # get the line's data transform         ax = line.get_axes()          sp1 = ax.transData.transform_point(p1)         sp2 = ax.transData.transform_point(p2)          rise = (sp2[1] - sp1[1])         run = (sp2[0] - sp1[0])          slope_degrees = math.degrees(math.atan(rise/run))          text.set_rotation(slope_degrees) 
like image 51
Adam Avatar answered Sep 24 '22 19:09

Adam