Built-in shortcuts in Matplotlib

In Matplotlib, it’s not too difficult to make a legend ( example_legend() , below), but I think it’s better to erase the labels directly on the curves built (as in example_inline() , below). This can be very difficult, because I have to specify the coordinates manually, and if I reformat the plot, I will probably have to move the labels. Is there a way to automatically generate labels on curves in Matplotlib? Bonus points for the ability to orient the text at an angle corresponding to the corner of the curve.

 import numpy as np import matplotlib.pyplot as plt def example_legend(): plt.clf() x = np.linspace(0, 1, 101) y1 = np.sin(x * np.pi / 2) y2 = np.cos(x * np.pi / 2) plt.plot(x, y1, label='sin') plt.plot(x, y2, label='cos') plt.legend() 

Figure with legend

 def example_inline(): plt.clf() x = np.linspace(0, 1, 101) y1 = np.sin(x * np.pi / 2) y2 = np.cos(x * np.pi / 2) plt.plot(x, y1, label='sin') plt.plot(x, y2, label='cos') plt.text(0.08, 0.2, 'sin') plt.text(0.9, 0.2, 'cos') 

Figure with inline labels

+74
matplotlib charts coordinates
Jun 07 '13 at 19:59 on
source share
3 answers

Good question, some time ago I experimented a bit with this, but did not use it a lot, because it is still not bulletproof. I divided the plot area into a 32x32 grid and calculated the “potential field” for the best label position for each row according to the following rules:

  • space is a good place for shortcut
  • The label should be next to the corresponding line.
  • The label should be away from other lines.

The code was something like this:

 import matplotlib.pyplot as plt import numpy as np from scipy import ndimage def my_legend(axis = None): if axis == None: axis = plt.gca() N = 32 Nlines = len(axis.lines) print Nlines xmin, xmax = axis.get_xlim() ymin, ymax = axis.get_ylim() # the 'point of presence' matrix pop = np.zeros((Nlines, N, N), dtype=np.float) for l in range(Nlines): # get xy data and scale it to the NxN squares xy = axis.lines[l].get_xydata() xy = (xy - [xmin,ymin]) / ([xmax-xmin, ymax-ymin]) * N xy = xy.astype(np.int32) # mask stuff outside plot mask = (xy[:,0] >= 0) & (xy[:,0] < N) & (xy[:,1] >= 0) & (xy[:,1] < N) xy = xy[mask] # add to pop for p in xy: pop[l][tuple(p)] = 1.0 # find whitespace, nice place for labels ws = 1.0 - (np.sum(pop, axis=0) > 0) * 1.0 # don't use the borders ws[:,0] = 0 ws[:,N-1] = 0 ws[0,:] = 0 ws[N-1,:] = 0 # blur the pop's for l in range(Nlines): pop[l] = ndimage.gaussian_filter(pop[l], sigma=N/5) for l in range(Nlines): # positive weights for current line, negative weight for others.... w = -0.3 * np.ones(Nlines, dtype=np.float) w[l] = 0.5 # calculate a field p = ws + np.sum(w[:, np.newaxis, np.newaxis] * pop, axis=0) plt.figure() plt.imshow(p, interpolation='nearest') plt.title(axis.lines[l].get_label()) pos = np.argmax(p) # note, argmax flattens the array first best_x, best_y = (pos / N, pos % N) x = xmin + (xmax-xmin) * best_x / N y = ymin + (ymax-ymin) * best_y / N axis.text(x, y, axis.lines[l].get_label(), horizontalalignment='center', verticalalignment='center') plt.close('all') x = np.linspace(0, 1, 101) y1 = np.sin(x * np.pi / 2) y2 = np.cos(x * np.pi / 2) y3 = x * x plt.plot(x, y1, 'b', label='blue') plt.plot(x, y2, 'r', label='red') plt.plot(x, y3, 'g', label='green') my_legend() plt.show() 

And the resulting plot: enter image description here

+24
Jun 08 '13 at 14:47
source share

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:

semi-automatic plot-labeling

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() 
+54
Sep 09 '16 at 1:27
source share

@Jan Kuiken's answer is, of course, thoughtful and thorough, but there are some caveats:

  • this does not work in all cases
  • this requires a fair amount of additional code
  • it can vary significantly from one site to another

A much simpler approach is to annotate the last point of each chart. The point can also be circled, for emphasis. This can be done with one additional line:

 from matplotlib import pyplot as plt for i, (x, y) in enumerate(samples): plt.plot(x, y) plt.text(x[-1], y[-1], 'sample {i}'.format(i=i)) 

The option will use ax.annotate .

+35
Apr 19 '15 at 1:37
source share



All Articles