14

The contour plot demo shows how you can plot the curves with the level value plotted over them, see below.

enter image description here

Is there a way to do this same thing for a simple line plot like the one obtained with the code below?

import matplotlib.pyplot as plt 

x = [1.81,1.715,1.78,1.613,1.629,1.714,1.62,1.738,1.495,1.669,1.57,1.877,1.385]
y = [0.924,0.915,0.914,0.91,0.909,0.905,0.905,0.893,0.886,0.881,0.873,0.873,0.844]

# This is the string that should show somewhere over the plotted line.
line_string = 'name of line'

# plotting
plt.plot(x,y)
plt.show()
Gabriel
  • 40,504
  • 73
  • 230
  • 404
  • Do you looking for automatic placement or just simple annotations? – Jakob Nov 09 '13 at 20:50
  • As far as I can tell the levels are placed automatically in a contour plot, so automatic placement would be ok. If there's a way to decide where the string should be located, even better I guess. – Gabriel Nov 09 '13 at 20:57

2 Answers2

17

You could simply add some text (MPL Gallery) like

import matplotlib.pyplot as plt 
import numpy as np
x = [1.81,1.715,1.78,1.613,1.629,1.714,1.62,1.738,1.495,1.669,1.57,1.877,1.385]
y = [0.924,0.915,0.914,0.91,0.909,0.905,0.905,0.893,0.886,0.881,0.873,0.873,0.844]

# This is the string that should show somewhere over the plotted line.
line_string = 'name of line'

# plotting
fig, ax = plt.subplots(1,1)
l, = ax.plot(x,y)
pos = [(x[-2]+x[-1])/2., (y[-2]+y[-1])/2.]
# transform data points to screen space
xscreen = ax.transData.transform(zip(x[-2::],y[-2::]))
rot = np.rad2deg(np.arctan2(*np.abs(np.gradient(xscreen)[0][0][::-1])))
ltex = plt.text(pos[0], pos[1], line_string, size=9, rotation=rot, color = l.get_color(),
     ha="center", va="center",bbox = dict(ec='1',fc='1'))

def updaterot(event):
    """Event to update the rotation of the labels"""
    xs = ax.transData.transform(zip(x[-2::],y[-2::]))
    rot = np.rad2deg(np.arctan2(*np.abs(np.gradient(xs)[0][0][::-1])))
    ltex.set_rotation(rot)

fig.canvas.mpl_connect('button_release_event', updaterot)
plt.show()

which gives
enter image description here

This way you have maximum control.
Note, the rotation is in degrees and in screen not data space.

Update:

As I recently needed automatic label rotations which update on zooming and panning, thus I updated my answer to account for these needs. Now the label rotation is updated on every mouse button release (the draw_event alone was not triggered when zooming). This approach uses matplotlib transformations to link the data and screen space as discussed in this tutorial.

Jakob
  • 19,815
  • 6
  • 75
  • 94
  • This is a great answer, thank you very much. I'll make a new question regarding the rotation angle if I can't figure it out myself (or even if I do, for the sake of completeness) Cheers. – Gabriel Nov 10 '13 at 04:52
  • I had to replace ```zip(x[-2::],y[-2::])``` with ```np.array((x[-2::],y[-2::]))``` for the transform call to work. – nedlrichards Feb 13 '20 at 23:14
6

Based on Jakob's code, here is a function that rotates the text in data space, puts labels near a given x or y data coordinate, and works also with log plots.

def label_line(line, label_text, near_i=None, near_x=None, near_y=None, rotation_offset=0, offset=(0,0)):
    """call 
        l, = plt.loglog(x, y)
        label_line(l, "text", near_x=0.32)
    """
    def put_label(i):
        """put label at given index"""
        i = min(i, len(x)-2)
        dx = sx[i+1] - sx[i]
        dy = sy[i+1] - sy[i]
        rotation = np.rad2deg(math.atan2(dy, dx)) + rotation_offset
        pos = [(x[i] + x[i+1])/2. + offset[0], (y[i] + y[i+1])/2 + offset[1]]
        plt.text(pos[0], pos[1], label_text, size=9, rotation=rotation, color = line.get_color(),
        ha="center", va="center", bbox = dict(ec='1',fc='1'))

    x = line.get_xdata()
    y = line.get_ydata()
    ax = line.get_axes()
    if ax.get_xscale() == 'log':
        sx = np.log10(x)    # screen space
    else:
        sx = x
    if ax.get_yscale() == 'log':
        sy = np.log10(y)
    else:
        sy = y

    # find index
    if near_i is not None:
        i = near_i
        if i < 0: # sanitize negative i
            i = len(x) + i
        put_label(i)
    elif near_x is not None:
        for i in range(len(x)-2):
            if (x[i] < near_x and x[i+1] >= near_x) or (x[i+1] < near_x and x[i] >= near_x):
                put_label(i)
    elif near_y is not None:
        for i in range(len(y)-2):
            if (y[i] < near_y and y[i+1] >= near_y) or (y[i+1] < near_y and y[i] >= near_y):
                put_label(i)
    else:
        raise ValueError("Need one of near_i, near_x, near_y")
  • 1
    I'm not sure whether you intentionally wanted this, but the way your code is right now, the label rotation is only the same angle as the curve when the x and y axis have the same scale. If you want the label to be rotated to the curve, you should do a transformation like that in Jakob's answer. – NauticalMile Feb 27 '16 at 03:20
  • 1
    Also, a note anyone else making use of this code: **Make sure you only add text using this method *after* your plot is finished.** If you add the labels as whenever you plot a curve, your x and/or y axis scales may be adjusted, and the orientation of the labels on the final plot will not be correct. – NauticalMile Feb 27 '16 at 03:24