Etykiety Inline w Matplotlib

W Matplotlib nie jest zbyt trudno stworzyć legendę (example_legend(), poniżej), ale myślę, że lepszym stylem jest umieszczanie etykiet bezpośrednio na wykreślanych krzywych (jak w example_inline(), poniżej). Może to być bardzo skrzypiące, ponieważ muszę ręcznie określać współrzędne, a jeśli przeformatuję Wykres, prawdopodobnie będę musiał zmienić położenie etykiet. Czy istnieje sposób na automatyczne generowanie etykiet na krzywych w Matplotlib? Punkty bonusowe za możliwość ustawienia tekstu pod kątem odpowiadającym kątowi krzywa.

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

Postać 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 etykietami inline

Author: RPichioli, 2013-06-07

3 answers

[1]] ładne pytanie, jakiś czas temu trochę z tym eksperymentowałem, ale nie używałem go zbyt często, bo nadal nie jest kuloodporny. Podzieliłem obszar wykresu na siatkę 32x32 i obliczyłem "pole potencjału" dla najlepszej pozycji etykiety dla każdej linii zgodnie z następującymi zasadami:

  • biała przestrzeń jest dobrym miejscem na etykietę
  • etykieta powinna znajdować się w pobliżu odpowiedniego wiersza
  • etykieta powinna być oddalona od pozostałych linii

Kod był czymś w rodzaju to:

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 wynikowy Wykres: Tutaj wpisz opis obrazka

 23
Author: Jan Kuiken,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-08 14:47:09

Update: użytkownik cphyc uprzejmie utworzył repozytorium Github dla kodu w tej odpowiedzi (zobacz tutaj ) i połączył kod w pakiet, który można zainstalować za pomocą pip install matplotlib-label-lines.


Ładne Zdjęcie:

półautomatyczne etykietowanie

W matplotlib bardzo łatwo jest etykietować wykresy konturu (automatycznie lub ręcznie umieszczając etykiety za pomocą kliknięć myszką). Nie ma (jeszcze) żadnej równoważnej możliwości oznaczania seria danych w ten sposób! Może być jakiś semantyczny powód, dla którego nie włączam tej funkcji, której mi brakuje.

Niezależnie od tego, napisałem następujący moduł, który przyjmuje dowolne pozwala na półautomatyczne etykietowanie Wykresów. Wymaga tylko numpy i kilku funkcji ze standardowej biblioteki math.

Opis

Domyślnym zachowaniem funkcji labelLines jest równomierne rozmieszczanie etykiet wzdłuż osi x (automatyczne umieszczanie na właściwej wartości y oczywiście). Jeśli chcesz, możesz po prostu przekazać tablicę współrzędnych x każdej z etykiet. Możesz nawet dostosować lokalizację jednej etykiety (jak pokazano na dolnym prawym wykresie), a resztę równomiernie rozmieścić, jeśli chcesz.

Dodatkowo, funkcja label_lines nie uwzględnia linii, które nie miały etykiety przypisanej w poleceniu plot (lub dokładniej, jeśli etykieta zawiera '_line').

Argumenty słów kluczowych przekazywane do labelLines lub labelLine są przekazywane do funkcji text wywołanie (niektóre argumenty słów kluczowych są ustawiane, jeśli kod wywołujący nie chce podać).

Problemy

  • obramowania adnotacji czasami zakłócają niepożądanie inne krzywe. Jak pokazano w adnotacjach 1 i 10 w lewym górnym rogu wykresu. Nie jestem nawet pewien, czy można tego uniknąć.
  • byłoby miło czasem określić y pozycję.
  • To wciąż iteracyjny proces, aby uzyskać adnotacje we właściwym miejscu.]}
  • działa tylko wtedy, gdy wartości osi x to floats

Gotchas

  • domyślnie funkcja labelLines zakłada, że wszystkie serie danych obejmują zakres określony przez granice osi. Spójrz na niebieską krzywą w lewym górnym rogu ładnego obrazu. Gdyby dostępne były tylko dane dla zakresu x0.5-1 następnie nie moglibyśmy umieścić etykiety w pożądanym miejscu(które jest nieco mniejsze niż 0.2). Zobacz to pytanie dla szczególnie paskudnego przykładu. W tej chwili kod nie identyfikuje inteligentnie tego scenariusza i nie układa etykiet, jednak istnieje rozsądne obejście. Funkcja labelLines przyjmuje argument xvals; listę x - wartości podanych przez użytkownika zamiast domyślnego rozkładu liniowego na całej szerokości. W ten sposób użytkownik może zdecydować, które wartości x - użyć do umieszczenia etykiet dla każdej serii danych.

Również, uważam, że jest to pierwsza odpowiedź, aby zakończyć bonus cel wyrównanie etykiet do krzywej, 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)

Kod testowy do wygenerowania pięknego zdjęcia powyżej:

from matplotlib import pyplot as plt
from scipy.stats import loglaplace,chi2

from label_lines 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()
 44
Author: NauticalMile,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-16 13:17:09

@ Jan Kuiken odpowiedź jest z pewnością przemyślana i dokładna, ale są pewne zastrzeżenia:

  • nie działa we wszystkich przypadkach
  • wymaga sporej ilości dodatkowego kodu
  • może się znacznie różnić od jednej działki do następnej
Znacznie prostszym podejściem jest przypisanie ostatniego punktu każdego wykresu. Punkt może być również zakreślony, dla podkreślenia. Można to osiągnąć 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))

Wariantem byłoby użycie ax.annotate.

 31
Author: Ioannis Filippidis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-14 10:59:15