Update: cphyc user kindly created a Github repository for the code in this answer (see here ) and put the code into a package that can be installed using pip install matplotlib-label-lines .
Nice picture:

In matplotlib quite easy to mark contour graphs (either automatically or manually placing labels with mouse clicks). It seems that (so far) there is no equivalent ability to designate data series in this way! There may be some semantic reason not to include this feature, which I am missing.
Despite this, I wrote the following module, which accepts any that allows semi-automatic marking of the schedule. Only numpy and a couple of functions from the standard math library are required.
Description
The behavior of the labelLines function by labelLines is to evenly place labels along the x axis (of course, automatically placing them in the correct y -value). If you want, you can simply pass an X coordinate array to each of the labels. You can even adjust the location of one label (as shown in the lower right graph) and evenly distribute the rest if you want.
In addition, the label_lines function label_lines not consider lines for which no label was assigned in the plot command (or, more precisely, if the label contains '_line' ).
Keyword arguments passed to labelLines or labelLine are passed to the call to the text function (some keyword arguments are set if the calling code decides not to specify).
the questions
- The bounding box of annotations sometimes undesirably interferes with other curves. As shown
1 and 10 annotations in the upper left graph. I'm not even sure that this can be avoided. - It would be nice sometimes to indicate the position of
y . - It is still an iterative process to get annotations in the right place.
- Only works when
x -axis values are numbers with float
Gotchas
- By default, the
labelLines function assumes that all data series cover a range defined by the axis. Take a look at the blue curve in the upper left graph of the beautiful picture. If only data were available for the range x 0.5 - 1 then we would not be able to place the label in the desired location (which is slightly less than 0.2 ). See this question for a particularly unpleasant example. Currently, the code does not reasonably define this script and does not reorder the labels, however there is a reasonable workaround. The labelLines function takes an argument xvals ; a list of x -values specified by the user instead of the default linear width distribution Thus, the user can decide which x -value to use for placing labels in each data series.
In addition, I believe that this is the first answer that completes the bonus task of aligning the marks with the curve on which they are located. :)
label_lines.py:
from math import atan2,degrees import numpy as np #Label line with line2D label data def labelLine(line,x,label=None,align=True,**kwargs): ax = line.axes xdata = line.get_xdata() ydata = line.get_ydata() if (x < xdata[0]) or (x > xdata[-1]): print('x label location is outside data range!') return #Find corresponding y co-ordinate and angle of the line ip = 1 for i in range(len(xdata)): if x < xdata[i]: ip = i break y = ydata[ip-1] + (ydata[ip]-ydata[ip-1])*(x-xdata[ip-1])/(xdata[ip]-xdata[ip-1]) if not label: label = line.get_label() if align: #Compute the slope dx = xdata[ip] - xdata[ip-1] dy = ydata[ip] - ydata[ip-1] ang = degrees(atan2(dy,dx)) #Transform to screen co-ordinates pt = np.array([x,y]).reshape((1,2)) trans_angle = ax.transData.transform_angles(np.array((ang,)),pt)[0] else: trans_angle = 0 #Set a bunch of keyword arguments if 'color' not in kwargs: kwargs['color'] = line.get_color() if ('horizontalalignment' not in kwargs) and ('ha' not in kwargs): kwargs['ha'] = 'center' if ('verticalalignment' not in kwargs) and ('va' not in kwargs): kwargs['va'] = 'center' if 'backgroundcolor' not in kwargs: kwargs['backgroundcolor'] = ax.get_facecolor() if 'clip_on' not in kwargs: kwargs['clip_on'] = True if 'zorder' not in kwargs: kwargs['zorder'] = 2.5 ax.text(x,y,label,rotation=trans_angle,**kwargs) def labelLines(lines,align=True,xvals=None,**kwargs): ax = lines[0].axes labLines = [] labels = [] #Take only the lines which have labels other than the default ones for line in lines: label = line.get_label() if "_line" not in label: labLines.append(line) labels.append(label) if xvals is None: xmin,xmax = ax.get_xlim() xvals = np.linspace(xmin,xmax,len(labLines)+2)[1:-1] for line,x,label in zip(labLines,xvals,labels): labelLine(line,x,label,align,**kwargs)
Test code to generate a beautiful picture above:
from matplotlib import pyplot as plt from scipy.stats import loglaplace,chi2 from labellines import * X = np.linspace(0,1,500) A = [1,2,5,10,20] funcs = [np.arctan,np.sin,loglaplace(4).pdf,chi2(5).pdf] plt.subplot(221) for a in A: plt.plot(X,np.arctan(a*X),label=str(a)) labelLines(plt.gca().get_lines(),zorder=2.5) plt.subplot(222) for a in A: plt.plot(X,np.sin(a*X),label=str(a)) labelLines(plt.gca().get_lines(),align=False,fontsize=14) plt.subplot(223) for a in A: plt.plot(X,loglaplace(4).pdf(a*X),label=str(a)) xvals = [0.8,0.55,0.22,0.104,0.045] labelLines(plt.gca().get_lines(),align=False,xvals=xvals,color='k') plt.subplot(224) for a in A: plt.plot(X,chi2(5).pdf(a*X),label=str(a)) lines = plt.gca().get_lines() l1=lines[-1] labelLine(l1,0.6,label=r'$Re=${}'.format(l1.get_label()),ha='left',va='bottom',align = False) labelLines(lines[:-1],align=False) plt.show()