Étiquettes en ligne dans Matplotlib

100

Dans Matplotlib, ce n'est pas trop difficile de créer une légende ( example_legend()ci-dessous), mais je pense qu'il est préférable de mettre des étiquettes directement sur les courbes tracées (comme example_inline()ci-dessous). Cela peut être très compliqué, 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 avec légende

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 avec des étiquettes en ligne

Alex Szatmary
la source

Réponses:

28

Bonne question, il y a quelque temps, j'ai un peu expérimenté cela, mais je ne l'ai pas beaucoup utilisé car il 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 selon les règles suivantes:

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

Le code était quelque chose comme ça:

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: entrez la description de l'image ici

Jan Kuiken
la source
Très agréable. Cependant, j'ai un exemple qui ne fonctionne pas complètement: plt.plot(x2, 3*x2**2, label="3x*x"); plt.plot(x2, 2*x2**2, label="2x*x"); plt.plot(x2, 0.5*x2**2, label="0.5x*x"); plt.plot(x2, -1*x2**2, label="-x*x"); plt.plot(x2, -2.5*x2**2, label="-2.5*x*x"); my_legend();cela place l'une des étiquettes dans le coin supérieur gauche. Des idées pour résoudre le problème? Il semble que le problème soit peut-être que les lignes sont trop rapprochées.
egpbos
Désolé, j'ai oublié x2 = np.linspace(0,0.5,100).
egpbos
Y a-t-il un moyen d'utiliser cela sans scipy? Sur mon système actuel, l'installation est difficile.
AnnanFay
Cela ne fonctionne pas pour moi sous Python 3.6.4, Matplotlib 2.1.2 et Scipy 1.0.0. Après avoir mis à jour la printcommande, il s'exécute et crée 4 tracés, dont 3 semblent être du charabia pixélisé (probablement quelque chose à voir avec le 32x32), et le quatrième avec des étiquettes à des endroits impairs.
Y Davis
84

Mise à jour: l' utilisateur cphyc a gentiment créé un référentiel Github pour le code de cette réponse (voir ici ) et a regroupé le code dans un package qui peut être installé à l'aide de pip install matplotlib-label-lines.


Belle photo:

étiquetage semi-automatique des tracés

Il matplotlibest assez facile d' étiqueter les tracés de contour (soit automatiquement, soit en plaçant manuellement des étiquettes avec des clics de souris). Il ne semble pas (encore) y avoir de capacité équivalente pour étiqueter des séries de données de cette façon! Il peut y avoir une raison sémantique pour ne pas inclure cette fonctionnalité qui me manque.

Quoi qu'il en soit, j'ai écrit le module suivant qui prend tout permet l'étiquetage semi-automatique des tracés. Il ne nécessite que numpyquelques fonctions de la mathbibliothèque standard .

La description

Le comportement par défaut de la labelLinesfonction est d'espacer les étiquettes uniformément le long de l' xaxe (en les plaçant automatiquement à la bonne valeur ybien 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 label_linesfonction ne tient pas compte des lignes qui n'ont pas eu d'étiquette affectée dans la plotcommande (ou plus précisément si l'étiquette contient '_line').

Arguments de mot-clé passés labelLinesou labelLinetransmis à l' textappel de fonction (certains arguments de mot-clé sont définis si le code appelant choisit de ne pas spécifier).

Problèmes

  • Les cadres de délimitation des annotations interfèrent parfois de manière indésirable avec d'autres courbes. Comme indiqué par les annotations 1et 10dans le graphique en haut à gauche. Je ne suis même pas sûr que cela puisse être évité.
  • Ce serait bien de spécifier une yposition à la place parfois.
  • C'est toujours un processus itératif pour obtenir des annotations au bon endroit
  • Cela ne fonctionne que lorsque les xvaleurs de -axis sont floats

Gotchas

  • Par défaut, la labelLinesfonction suppose que toutes les séries de données couvrent la plage spécifiée par les limites de l'axe. Jetez un œil à la courbe bleue dans le tracé en haut à gauche de la jolie image. S'il n'y avait que des données disponibles pour la xplage 0.5- 1alors nous ne pourrions pas placer une étiquette à l'emplacement souhaité (qui est un peu moins que 0.2). Voir cette question pour un exemple particulièrement désagréable. À 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' xvalsargument; une liste de xvaleurs spécifiées par l'utilisateur au lieu de la distribution linéaire par défaut sur la largeur. Ainsi, l'utilisateur peut décider lequelx-valeurs à utiliser pour le placement d'étiquette de chaque série de données.

De plus, je pense que c'est la première réponse pour atteindre l' objectif bonus d'aligner les étiquettes avec 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)

Testez le code 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()
Mile nautique
la source
1
@blujay Je suis content que vous ayez pu l'adapter à vos besoins. J'ajouterai cette contrainte comme problème.
NauticalMile
1
@Liza Lisez mon Gotcha Je viens d'ajouter pourquoi cela se produit. Pour votre cas (je suppose que c'est comme celui de cette question ) à moins que vous ne souhaitiez créer manuellement une liste de xvals, vous voudrez peut-être modifier labelLinesun peu le code: changez le code sous la if xvals is None:portée pour créer une liste basée sur d'autres critères. Vous pourriez commencer avecxvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
NauticalMile
1
@Liza Votre graphique m'intrigue cependant. Le problème est que vos données ne sont pas réparties uniformément sur le tracé et que vous avez beaucoup de courbes qui sont presque les unes sur les autres. Avec ma solution, il peut être très difficile de distinguer les étiquettes dans de nombreux cas. Je pense que la meilleure solution est d'avoir des blocs d'étiquettes empilées dans différentes parties vides de votre intrigue. Voir ce graphique pour un exemple avec deux blocs d'étiquettes empilées (un bloc avec 1 étiquette et un autre bloc avec 4). Mettre en œuvre cela serait un peu de travail de fond, je pourrais le faire à un moment donné dans le futur.
NauticalMile
1
Remarque: depuis Matplotlib 2.0, .get_axes()et .get_axis_bgcolor()sont obsolètes. Veuillez remplacer par .axeset .get_facecolor(), resp.
Jiāgěng
1
Une autre chose géniale labellinesest que les propriétés s'y rapportent plt.textou s'y ax.textappliquent. Cela signifie que vous pouvez définir fontsizeet bboxparamètres dans la labelLines()fonction.
tionichm
52

La réponse de @Jan Kuiken est certainement bien pensée et approfondie, mais il y a quelques mises en garde:

  • ça ne marche pas dans tous les cas
  • cela 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é, pour mettre l'accent. Cela 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 à utiliser ax.annotate.

Ioannis Filippidis
la source
1
+1! Cela ressemble à une solution simple et agréable. Désolé pour la paresse, mais à quoi cela ressemblerait-il? Le texte se trouverait-il à l'intérieur du tracé ou au-dessus de l'axe y droit?
rocarvaj
1
@rocarvaj Cela dépend d'autres paramètres. Il est possible que les étiquettes dépassent de la zone de traçage. Deux façons d'éviter ce comportement sont: 1) utiliser un index différent de -1, 2) définir des limites d'axe appropriées pour laisser de l'espace pour les étiquettes.
Ioannis Filippidis le
1
Cela devient également un gâchis, si les graphiques se concentrent sur une valeur y - les points de terminaison deviennent trop proches pour que le texte soit beau
LazyCat
@LazyCat: C'est vrai. Pour résoudre ce problème, on peut rendre les annotations déplaçables. Un peu de douleur je suppose mais ça ferait l'affaire.
PlacidLush
1

Une approche plus simple comme celle de Ioannis Filippidis:

import matplotlib.pyplot as plt
import numpy as np

# evenly sampled time at 200ms intervals
tMin=-1 ;tMax=10
t = np.arange(tMin, tMax, 0.1)

# red dashes, blue points default
plt.plot(t, 22*t, 'r--', t, t**2, 'b')

factor=3/4 ;offset=20  # text position in view  
textPosition=[(tMax+tMin)*factor,22*(tMax+tMin)*factor]
plt.text(textPosition[0],textPosition[1]+offset,'22  t',color='red',fontsize=20)
textPosition=[(tMax+tMin)*factor,((tMax+tMin)*factor)**2+20]
plt.text(textPosition[0],textPosition[1]+offset, 't^2', bbox=dict(facecolor='blue', alpha=0.5),fontsize=20)
plt.show()

code python 3 sur sageCell

Tagada
la source