Label sebaris di Matplotlib

102

Di Matplotlib, tidak terlalu sulit untuk membuat legenda ( example_legend(), di bawah), tapi menurut saya gaya yang lebih baik adalah meletakkan label tepat pada kurva yang sedang diplot (seperti di example_inline(), di bawah). Ini bisa sangat rumit, karena saya harus menentukan koordinat dengan tangan, dan, jika saya memformat ulang plot, saya mungkin harus mengubah posisi label. Apakah ada cara untuk secara otomatis menghasilkan label pada kurva di Matplotlib? Poin bonus untuk dapat mengarahkan teks pada sudut yang sesuai dengan sudut kurva.

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

Sosok dengan legenda

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

Gambar dengan label sebaris

Alex Szatmary
sumber

Jawaban:

29

Pertanyaan bagus, beberapa waktu lalu saya sudah sedikit bereksperimen dengan ini, tapi belum banyak menggunakannya karena masih belum antipeluru. Saya membagi area plot menjadi kisi 32x32 dan menghitung 'bidang potensial' untuk posisi label terbaik untuk setiap baris sesuai aturan berikut:

  • ruang putih adalah tempat yang bagus untuk label
  • Label harus berada di dekat garis yang sesuai
  • Label harus jauh dari garis lainnya

Kodenya kira-kira seperti ini:

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

Dan plot yang dihasilkan: masukkan deskripsi gambar di sini

Jan Kuiken
sumber
Sangat bagus. Namun, saya memiliki contoh yang tidak sepenuhnya berfungsi: 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();Ini menempatkan salah satu label di sudut kiri atas. Ada ide tentang cara memperbaikinya? Sepertinya masalahnya mungkin garis-garisnya terlalu berdekatan.
egpbos
Maaf, lupa x2 = np.linspace(0,0.5,100).
egpbos
Apakah ada cara untuk menggunakan ini tanpa scipy? Pada sistem saya saat ini, sulit untuk menginstal.
AnnanFay
Ini tidak berfungsi untuk saya di bawah Python 3.6.4, Matplotlib 2.1.2, dan Scipy 1.0.0. Setelah memperbarui printperintah, itu berjalan dan membuat 4 plot, 3 di antaranya tampak seperti omong kosong pixelated (mungkin ada hubungannya dengan 32x32), dan yang keempat dengan label di tempat-tempat aneh.
Y Davis
84

Pembaruan: Pengguna cphyc telah dengan ramah membuat repositori Github untuk kode dalam jawaban ini (lihat di sini ), dan memaketkan kode ke dalam paket yang dapat diinstal menggunakan pip install matplotlib-label-lines.


Gambar yang indah:

pelabelan plot semi-otomatis

Di matplotlibdalamnya cukup mudah untuk memberi label pada plot kontur (baik secara otomatis atau dengan menempatkan label secara manual dengan klik mouse). Tampaknya belum ada kemampuan yang setara untuk melabeli seri data dengan cara ini! Mungkin ada beberapa alasan semantik untuk tidak menyertakan fitur ini yang saya lewatkan.

Terlepas dari itu, saya telah menulis modul berikut yang memungkinkan pelabelan plot semi-otomatis. Ini hanya membutuhkan numpydan beberapa fungsi dari mathperpustakaan standar .

Deskripsi

Perilaku default dari labelLinesfungsi ini adalah memberi jarak label secara merata di sepanjang xsumbu (tentu saja secara otomatis menempatkan pada nilai yang benar y). Jika mau, Anda bisa meneruskan larik koordinat x dari masing-masing label. Anda bahkan dapat mengubah lokasi satu label (seperti yang ditunjukkan di plot kanan bawah) dan memberi jarak pada label lainnya secara merata jika Anda mau.

Selain itu, label_linesfungsi tersebut tidak memperhitungkan baris yang belum diberi label pada plotperintah (atau lebih akurat jika label berisi '_line').

Argumen kata kunci diteruskan ke labelLinesatau labelLinediteruskan ke textpemanggilan fungsi (beberapa argumen kata kunci disetel jika kode pemanggil memilih untuk tidak menentukan).

Masalah

  • Kotak pembatas anotasi terkadang mengganggu kurva lain yang tidak diinginkan. Seperti yang ditunjukkan oleh anotasi 1dan 10di plot kiri atas. Saya bahkan tidak yakin ini bisa dihindari.
  • Akan lebih baik untuk menentukan yposisi kadang-kadang.
  • Ini masih merupakan proses berulang untuk mendapatkan anotasi di lokasi yang tepat
  • Ini hanya bekerja jika nilai x-axis adalah floats

Gotchas

  • Secara default, labelLinesfungsi mengasumsikan bahwa semua rangkaian data menjangkau rentang yang ditentukan oleh batas sumbu. Perhatikan kurva biru di kiri atas gambar cantik. Jika hanya ada data yang tersedia untuk xrentang tersebut 0.5- 1maka kami tidak mungkin menempatkan label di lokasi yang diinginkan (yang sedikit kurang dari 0.2). Lihat pertanyaan ini untuk contoh yang sangat buruk. Saat ini, kode tidak dengan cerdas mengidentifikasi skenario ini dan mengatur ulang label, namun ada solusi yang masuk akal. Fungsi labelLines mengambil xvalsargumen; daftar- xnilai yang ditentukan oleh pengguna, bukan distribusi linier default di seluruh lebar. Sehingga pengguna bisa memutuskan yang manax-nilai yang akan digunakan untuk penempatan label dari setiap seri data.

Juga, saya yakin ini adalah jawaban pertama untuk menyelesaikan tujuan bonus dalam menyelaraskan label dengan kurva tempat mereka berada. :)

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)

Uji kode untuk menghasilkan gambar cantik di atas:

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()
Mil laut
sumber
1
@blujay Saya senang Anda dapat menyesuaikannya dengan kebutuhan Anda. Saya akan menambahkan kendala itu sebagai masalah.
NauticalMile
1
@Liza Baca Gotcha saya, saya baru saja menambahkan mengapa ini terjadi. Untuk kasus Anda (saya berasumsi bahwa ini seperti yang ada dalam pertanyaan ini ) kecuali Anda ingin membuat daftar secara manual xvals, Anda mungkin ingin sedikit memodifikasi labelLineskode: ubah kode di bawah if xvals is None:cakupan untuk membuat daftar berdasarkan kriteria lain. Anda bisa mulai denganxvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
NauticalMile
1
@ Liza Grafik Anda membuat saya penasaran. Masalahnya adalah data Anda tidak tersebar merata di seluruh plot, dan Anda memiliki banyak kurva yang hampir bertumpuk satu sama lain. Dengan solusi saya, mungkin sangat sulit untuk membedakan label dalam banyak kasus. Saya pikir solusi terbaik adalah memiliki blok label bertumpuk di berbagai bagian kosong dari plot Anda. Lihat grafik ini untuk contoh dengan dua blok label bertumpuk (satu blok dengan 1 label, dan blok lainnya dengan 4). Menerapkan ini akan menjadi sedikit kerja keras, saya mungkin melakukannya di beberapa titik di masa depan.
NauticalMile
1
Catatan: sejak Matplotlib 2.0, .get_axes()dan .get_axis_bgcolor()sudah tidak digunakan lagi. Harap ganti dengan .axesdan .get_facecolor(), resp.
Jiāgěng
1
Hal hebat lainnya labellinesadalah properti yang terkait plt.textatau ax.textberlaku untuk itu. Artinya Anda dapat mengatur fontsizedan bboxparameter dalam labelLines()fungsi tersebut.
tionichm
53

Jawaban @Jan Kuiken tentu saja dipikirkan dengan matang dan menyeluruh, tetapi ada beberapa peringatan:

  • itu tidak bekerja di semua kasus
  • itu membutuhkan cukup banyak kode tambahan
  • ini mungkin sangat bervariasi dari satu plot ke plot berikutnya

Pendekatan yang jauh lebih sederhana adalah memberi anotasi pada poin terakhir dari setiap plot. Intinya juga bisa dilingkari, untuk penekanan. Ini dapat dilakukan dengan satu baris tambahan:

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

Varian akan digunakan ax.annotate.

Ioannis Filippidis
sumber
1
+1! Sepertinya solusi yang bagus dan sederhana. Maaf atas kemalasannya, tapi bagaimana tampilannya? Apakah teks berada di dalam plot atau di atas sumbu y kanan?
rocarvaj
1
@rocarvaj Itu tergantung pada pengaturan lain. Label mungkin saja menonjol di luar kotak plot. Dua cara untuk menghindari perilaku ini adalah: 1) menggunakan indeks yang berbeda dari -1, 2) menetapkan batas sumbu yang sesuai untuk memberi ruang bagi label.
Ioannis Filippidis
1
Ini juga menjadi berantakan, jika plot berkonsentrasi pada beberapa nilai y - titik akhir menjadi terlalu dekat agar teks terlihat bagus
LazyCat
@ LazyCat: Itu benar. Untuk memperbaikinya, seseorang dapat membuat anotasi dapat diseret. Sedikit menyebalkan, tapi itu akan berhasil.
PlacidLush
1

Pendekatan yang lebih sederhana seperti yang dilakukan 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()

kode python 3 di sageCell

Tagada
sumber