web-dev-qa-db-fra.com

Étiquettes en ligne dans Matplotlib

Dans Matplotlib, il n'est pas difficile de créer une légende (example_legend(), ci-dessous), mais je pense qu'il est préférable de mettre des étiquettes sur les courbes en cours de tracé (comme dans example_inline(), ci-dessous ). Cela peut être très délicat, car je dois spécifier les coordonnées à la main et, si je reformate le tracé, je dois probablement repositionner les étiquettes. Existe-t-il un moyen de générer automatiquement des étiquettes sur les courbes dans Matplotlib? Points bonus pour pouvoir orienter le texte selon un angle correspondant à l'angle de la courbe.

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

77
Alex Szatmary

Belle question, il y a quelque temps, j'ai un peu expérimenté cela, mais je ne l'ai pas beaucoup utilisée parce qu'elle n'est toujours pas à l'épreuve des balles. J'ai divisé la zone de tracé en une grille 32x32 et calculé un "champ potentiel" pour la meilleure position d'une étiquette pour chaque ligne, conformément aux règles suivantes:

  • l'espace blanc est un bon endroit pour une étiquette
  • L'étiquette doit être proche de la ligne correspondante
  • L'étiquette doit être éloignée des autres lignes

Le code ressemblait à ceci:

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()

Et l'intrigue qui en résulte: enter image description here

25
Jan Kuiken

Mise à jour: L'utilisateur cphyc a gentiment créé un référentiel Github pour le code de cette réponse (voir ici =) et regroupé le code dans un paquet pouvant être installé avec pip install matplotlib-label-lines.


Belle photo:

semi-automatic plot-labeling

Dans matplotlib, il est assez facile de tracés de contour d'étiquette (soit automatiquement, soit en plaçant manuellement des étiquettes en un clic de souris). Il ne semble pas y avoir (encore) de capacité équivalente pour étiqueter des séries de données de cette manière! Il peut y avoir une raison sémantique pour ne pas inclure cette fonctionnalité qui me manque.

Quoiqu’il en soit, j’ai écrit le module suivant qui prend tout ce qui permet un étiquetage semi-automatique des parcelles. Il ne nécessite que numpy et quelques fonctions de la bibliothèque standard math.

La description

Le comportement par défaut de la fonction labelLines consiste à espacer uniformément les étiquettes le long de l'axe x (en plaçant automatiquement ce paramètre à la valeur correcte y- bien sûr). Si vous le souhaitez, vous pouvez simplement passer un tableau des coordonnées x de chacune des étiquettes. Vous pouvez même modifier l'emplacement d'une étiquette (comme indiqué dans le graphique en bas à droite) et espacer le reste de manière égale, si vous le souhaitez.

De plus, la fonction label_lines Ne prend pas en compte les lignes pour lesquelles aucune étiquette n'a été affectée dans la commande plot (ou plus précisément si l'étiquette contient '_line').

Les arguments de mot clé transmis à labelLines ou labelLine sont transmis à l'appel de fonction text (certains arguments de mot clé sont définis si le code appelant choisit de ne pas spécifier).

Problèmes

  • Les cadres de sélection des annotations interfèrent parfois de manière indésirable avec d'autres courbes. Comme indiqué par les annotations 1 Et 10 Dans le graphique en haut à gauche. Je ne suis même pas sûr que cela puisse être évité.
  • Il serait bien de spécifier parfois une position y à la place.
  • Obtenir des annotations au bon endroit reste un processus itératif
  • Cela ne fonctionne que lorsque les valeurs de x- axe sont floats

Gotchas

  • Par défaut, la fonction labelLines suppose que toutes les séries de données couvrent la plage spécifiée par les limites de l'axe. Regardez la courbe bleue dans le graphique en haut à gauche de la jolie image. S'il n'y avait que des données disponibles pour le x plage 0.5 - 1, Il serait alors impossible de placer une étiquette à l'emplacement souhaité (ce qui est un peu moins que 0.2). Voir cette question pour un exemple particulièrement méchant. À l'heure actuelle, le code n'identifie pas intelligemment ce scénario et ne réorganise pas les étiquettes, mais il existe une solution de contournement raisonnable. La fonction labelLines prend l'argument xvals; une liste de x- valeurs spécifiées par l'utilisateur au lieu de la distribution linéaire par défaut sur toute la largeur. L’utilisateur peut donc choisir les valeurs x- à utiliser pour l’emplacement des étiquettes de chaque série de données.

De plus, je crois que c’est la première réponse pour compléter l’objectif bonus d’aligner les étiquettes sur la courbe sur laquelle elles se trouvent. :)

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)

Code de test pour générer la jolie image ci-dessus:

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()
58
NauticalMile

La réponse de @Jan Kuiken est certes bien pensée et approfondie, mais il y a quelques réserves:

  • ça ne marche pas dans tous les cas
  • il nécessite une bonne quantité de code supplémentaire
  • il peut varier considérablement d'une parcelle à l'autre

Une approche beaucoup plus simple consiste à annoter le dernier point de chaque graphique. Le point peut également être encerclé, par exemple. Ceci peut être accompli avec une ligne supplémentaire:

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))

Une variante serait d'utiliser ax.annotate .

41
Ioannis Filippidis