Etykiety wbudowane w Matplotlib

102

W Matplotlib nie jest zbyt trudno stworzyć legendę ( example_legend()poniżej), ale myślę, że lepiej jest umieścić etykiety bezpośrednio na kreślonych krzywych (jak example_inline()poniżej). Może to być bardzo kłopotliwe, ponieważ muszę ręcznie określić współrzędne, a jeśli ponownie sformatuję działkę, prawdopodobnie będę musiał zmienić położenie etykiet. Czy istnieje sposób na automatyczne generowanie etykiet na krzywych w Matplotlib? Dodatkowe punkty za możliwość zorientowania tekstu pod kątem odpowiadającym kątowi krzywej.

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

Rysunek z 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')

Rysunek z wbudowanymi etykietami

Alex Szatmary
źródło

Odpowiedzi:

29

Dobre pytanie, jakiś czas temu trochę z tym eksperymentowałem, ale nie używałem go zbyt często, ponieważ nadal nie jest kuloodporny. Podzieliłem obszar kreślenia na siatkę 32x32 i obliczyłem `` pole potencjalne '' dla najlepszego położenia etykiety dla każdej linii zgodnie z następującymi zasadami:

  • białe miejsce to dobre miejsce na etykietę
  • Etykieta powinna znajdować się w pobliżu odpowiedniej linii
  • Etykieta powinna znajdować się z dala od innych linii

Kod wyglądał mniej więcej tak:

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

I wynikowa fabuła: wprowadź opis obrazu tutaj

Jan Kuiken
źródło
Bardzo dobrze. Mam jednak przykład, który nie działa całkowicie: 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();powoduje umieszczenie jednej z etykiet w lewym górnym rogu. Jakieś pomysły, jak to naprawić? Wygląda na to, że problem może polegać na tym, że linie są zbyt blisko siebie.
egpbos
Przepraszam, zapomniałem x2 = np.linspace(0,0.5,100).
egpbos
Czy jest jakiś sposób na użycie tego bez scipy? W moim obecnym systemie instalacja jest trudna.
AnnanFay
To nie działa dla mnie pod Pythonem 3.6.4, Matplotlib 2.1.2 i Scipy 1.0.0. Po zaktualizowaniu printpolecenia uruchamia się i tworzy 4 wykresy, z których 3 wydają się być pikselowym bełkotem (prawdopodobnie ma to związek z 32x32), a czwarty z etykietami w dziwnych miejscach.
Y Davis
84

Aktualizacja: Użytkownik cphyc uprzejmie utworzył repozytorium Github dla kodu w tej odpowiedzi (patrz tutaj ) i umieścił kod w pakiecie, który można zainstalować za pomocą pip install matplotlib-label-lines.


Ładne zdjęcie:

półautomatyczne etykietowanie działek

W matplotlibto dość łatwe do działek konturu etykieta (automatycznie lub ręcznie umieszczania etykiet z kliknięć myszką). Wydaje się, że nie ma (jeszcze) równoważnej możliwości opisywania serii danych w ten sposób! Może istnieć jakiś semantyczny powód, aby nie uwzględnić tej funkcji, której mi brakuje.

Mimo to napisałem następujący moduł, który przyjmuje dowolne opcje półautomatycznego etykietowania działek. Wymaga tylko numpyi kilku funkcji z mathbiblioteki standardowej .

Opis

Domyślnym zachowaniem labelLinesfunkcji jest równomierne rozmieszczenie etykiet wzdłuż xosi ( yoczywiście automatyczne umieszczenie na prawidłowej wartości). Jeśli chcesz, możesz po prostu przekazać tablicę współrzędnych x każdej z etykiet. Możesz nawet dostosować położenie jednej etykiety (jak pokazano na wykresie w prawym dolnym rogu) i równomiernie rozmieścić resztę, jeśli chcesz.

Ponadto label_linesfunkcja nie uwzględnia wierszy, które nie mają przypisanej etykiety w plotpoleceniu (lub dokładniej, jeśli etykieta zawiera '_line').

Argumenty słów kluczowych przekazywane labelLineslub labelLinesą przekazywane do textwywołania funkcji (niektóre argumenty słów kluczowych są ustawiane, jeśli kod wywołujący zdecyduje się nie określać).

Zagadnienia

  • Ramki ograniczające adnotacje czasami w niepożądany sposób kolidują z innymi krzywymi. Jak pokazano w adnotacjach 1i 10w lewym górnym rogu wykresu. Nie jestem nawet pewien, czy można tego uniknąć.
  • yPrzydałoby się czasem określić pozycję.
  • Nadal jest to proces iteracyjny, aby uzyskać adnotacje we właściwej lokalizacji
  • Działa tylko wtedy, gdy xwartości -axis są floats

Gotchas

  • Domyślnie labelLinesfunkcja zakłada, że ​​wszystkie serie danych obejmują zakres określony przez limity osi. Spójrz na niebieską krzywą w lewym górnym rogu tego ładnego obrazka. Gdyby dostępne były tylko dane dla xzakresu 0.5- 1wtedy nie moglibyśmy umieścić etykiety w żądanym miejscu (czyli trochę mniej niż 0.2). Zobacz to pytanie, aby zobaczyć szczególnie nieprzyjemny przykład. Obecnie kod nie identyfikuje w inteligentny sposób tego scenariusza i nie zmienia kolejności etykiet, jednak istnieje rozsądne obejście tego problemu. Funkcja labelLines przyjmuje xvalsargument; lista x-wartości określonych przez użytkownika zamiast domyślnego rozkładu liniowego na szerokości. Dzięki temu użytkownik może zdecydować, który plikx-wartości używane do umieszczania etykiet dla każdej serii danych.

Uważam również, że jest to pierwsza odpowiedź na realizację celu dodatkowego, jakim jest wyrównanie etykiet z krzywą, na której się znajdują. :)

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)

Przetestuj kod, aby wygenerować ładny obrazek powyżej:

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()
Mila morska
źródło
1
@blujay Cieszę się, że udało Ci się dostosować go do swoich potrzeb. Dodam to ograniczenie jako problem.
NauticalMile
1
@Liza Read my Gotcha Właśnie dodałem, dlaczego tak się dzieje. W twoim przypadku (zakładam, że jest podobny do tego w tym pytaniu ), chyba że chcesz ręcznie utworzyć listę xvals, możesz chcieć labelLinestrochę zmodyfikować kod: zmień kod w if xvals is None:zakresie, aby utworzyć listę opartą na innych kryteriach. Możesz zacząć odxvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
NauticalMile
1
@Liza Twój wykres mnie jednak intryguje. Problem polega na tym, że dane nie są równomiernie rozłożone na wykresie i masz wiele krzywych, które są prawie jedna na drugiej. Dzięki mojemu rozwiązaniu rozróżnienie etykiet może być w wielu przypadkach bardzo trudne. Myślę, że najlepszym rozwiązaniem jest umieszczenie bloków ułożonych w stos etykiet w różnych pustych częściach działki. Zobacz ten wykres, aby zobaczyć przykład z dwoma blokami skumulowanych etykiet (jeden blok z 1 etykietą, a drugi z 4). Wdrożenie tego wymagałoby sporo pracy, mógłbym to zrobić w pewnym momencie w przyszłości.
NauticalMile
1
Uwaga: od wersji Matplotlib 2.0 .get_axes()i .get_axis_bgcolor()są one przestarzałe. Proszę wymienić na .axesi .get_facecolor(), odp.
Jiāgěng
1
Kolejną niesamowitą rzeczą labellinesjest to, że właściwości są z nią powiązane plt.textlub mają ax.textdo niej zastosowanie. Oznacza to, że możesz ustawić fontsizei bboxparametry w labelLines()funkcji.
tionichm
53

Odpowiedź @Jana Kuikena jest z pewnością dobrze przemyślana i dokładna, ale są pewne zastrzeżenia:

  • nie działa we wszystkich przypadkach
  • wymaga sporo dodatkowego kodu
  • może się znacznie różnić w zależności od działki

O wiele prostszym podejściem jest dodanie adnotacji do ostatniego punktu każdego wykresu. Punkt można również zakreślić dla podkreślenia. Można to zrobić za pomocą jednej dodatkowej linii:

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

Wariant byłby do użycia ax.annotate.

Ioannis Filippidis
źródło
1
+1! Wygląda na ładne i proste rozwiązanie. Przepraszam za lenistwo, ale jak by to wyglądało? Czy tekst byłby wewnątrz wykresu, czy na górze prawej osi y?
rocarvaj
1
@rocarvaj To zależy od innych ustawień. Etykiety mogą wystawać poza obszar kreślenia. Istnieją dwa sposoby uniknięcia tego zachowania: 1) użycie indeksu innego niż -1, 2) ustawienie odpowiednich granic osi, aby zapewnić miejsce na etykiety.
Ioannis Filippidis
1
Staje się również bałagan, jeśli wykresy koncentrują się na jakiejś wartości y - punkty końcowe stają się zbyt blisko, aby tekst wyglądał ładnie
LazyCat
@LazyCat: To prawda. Aby to naprawić, można umożliwić przeciąganie adnotacji. Wydaje mi się, że to trochę bolesne, ale wystarczyłoby.
PlacidLush
1

Prostsze podejście, takie jak to, które robi 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 w sageCell

Tagada
źródło