Matplotlib'de satır içi etiketler


104

Matplotlib'de bir efsane yaratmak çok zor değil ( example_legend()aşağıda), ancak etiketleri çizilen eğrilerin üzerine yerleştirmenin daha iyi bir tarz olduğunu düşünüyorum ( example_inline()aşağıdaki gibi). Bu çok zor olabilir, çünkü koordinatları elle belirlemem gerekiyor ve grafiği yeniden biçimlendirirsem, muhtemelen etiketleri yeniden konumlandırmam gerekir. Matplotlib'de eğrilerde otomatik olarak etiket oluşturmanın bir yolu var mı? Metni, eğrinin açısına karşılık gelen bir açıyla yönlendirebilmek için bonus puanlar.

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

Açıklamalı figür

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

Satır içi etiketli şekil

Yanıtlar:


30

Güzel soru, bir süre önce bununla biraz deney yaptım, ancak pek kullanmadım çünkü hala kurşun geçirmez değil. Çizim alanını 32x32'lik bir ızgaraya böldüm ve aşağıdaki kurallara göre her satır için bir etiketin en iyi konumu için bir 'potansiyel alan' hesapladım:

  • beyaz boşluk bir etiket için iyi bir yerdir
  • Etiket karşılık gelen satıra yakın olmalıdır
  • Etiket diğer satırlardan uzakta olmalıdır

Kod şunun gibi bir şeydi:

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

Ve ortaya çıkan arsa: görüntü açıklamasını buraya girin


Çok hoş. Ancak, tam olarak çalışmayan bir 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();örneğim var : Bu, etiketlerden birini sol üst köşeye koyuyor. Bunu nasıl düzelteceğine dair bir fikrin var mı? Görünüşe göre sorun, çizgilerin birbirine çok yakın olması olabilir.
egpbos

Üzgünüm unuttum x2 = np.linspace(0,0.5,100).
egpbos

Bunu scipy olmadan kullanmanın bir yolu var mı? Şu anki sistemimde yüklemek çok zor.
AnnanFay

Bu benim için Python 3.6.4, Matplotlib 2.1.2 ve Scipy 1.0.0 altında çalışmıyor. printKomutu güncelledikten sonra , 3'ü pikselli anlamsız görünen (muhtemelen 32x32 ile ilgili bir şey) ve dördüncüsü tuhaf yerlerde etiketler bulunan 4 çizim çalıştırır ve oluşturur.
Y Davis

86

Güncelleme: Kullanıcı cphyc bu yanıttaki kod için nazikçe bir Github deposu oluşturdu ( buraya bakın ) ve kodu kullanılarak kurulabilecek bir pakete paketledi pip install matplotlib-label-lines.


Güzel bir resim:

yarı otomatik işaretleme

Gelen matplotlibbu oldukça kolaydır etiket kontur araziler (otomatik veya manuel fare tıklaması ile etiket koyarak ya). Bu şekilde veri serilerini etiketlemek için eşdeğer bir yetenek (henüz) görünmemektedir! Eksik olduğum bu özelliği dahil etmemek için bazı anlamsal nedenler olabilir.

Her şeye rağmen, yarı otomatik grafik etiketlemeye izin veren aşağıdaki modülü yazdım. numpyStandart mathkitaplıktan sadece ve birkaç işlev gerektirir .

Açıklama

labelLinesFonksiyonun varsayılan davranışı , etiketleri xeksen boyunca eşit aralıklarla yerleştirmektir y(elbette otomatik olarak doğru değere yerleştirilir ). İsterseniz, her bir etiketin x koordinat dizisini iletebilirsiniz. Hatta bir etiketin konumunu (sağ alttaki grafikte gösterildiği gibi) değiştirebilir ve isterseniz geri kalanını eşit aralıklarla yerleştirebilirsiniz.

Ek olarak, label_linesişlev, plotkomutta bir etiket atanmamış satırları hesaba katmaz (veya etiket içeriyorsa daha doğrusu '_line').

Kelime argümanlar geçirilen labelLinesveya labelLineaktarılmaktadır textişlev çağrısında (çağırarak kod seçer belirtmek değilse bazı anahtar kelime argümanlar ayarlanır).

Sorunlar

  • Ek açıklama sınırlayıcı kutuları bazen diğer eğrilere istenmeyen şekilde müdahale eder. Sol üstteki grafikte 1ve 10ek açıklamalarla gösterildiği gibi . Bunun önlenebileceğinden bile emin değilim.
  • yBazen bunun yerine bir pozisyon belirlemek güzel olurdu .
  • Ek açıklamaları doğru konuma almak için hala yinelemeli bir süreçtir
  • Yalnızca x-axis değerleri floats olduğunda çalışır

Gotchas

  • Varsayılan olarak labelLinesişlev, tüm veri serilerinin eksen sınırlarıyla belirtilen aralığı kapsadığını varsayar. Güzel resmin sol üst köşesindeki mavi eğriye bir bakın. xAralık için yalnızca veriler mevcut olsaydı 0.5- 1o zaman muhtemelen istenen konuma bir etiket yerleştiremezdik (bundan biraz daha küçüktür 0.2). Özellikle kötü bir örnek için bu soruya bakın . Şu anda, kod bu senaryoyu akıllıca tanımlamıyor ve etiketleri yeniden düzenlemiyor, ancak makul bir geçici çözüm var. LabelLines işlevi xvalsargümanı alır ; xgenişlik boyunca varsayılan doğrusal dağılım yerine kullanıcı tarafından belirtilen değerler listesi . Böylece kullanıcı hangisininx- Her veri serisinin etiket yerleşimi için kullanılacak değerler.

Ayrıca, etiketleri üzerinde bulundukları eğriye göre hizalama bonus hedefini tamamlamak için ilk cevabın bu olduğuna inanıyorum . :)

label_lines.py: 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)

Yukarıdaki güzel resmi oluşturmak için kodu test edin:

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

1
@ blujay İhtiyaçlarınıza göre uyarlayabildiğinize sevindim. Bu kısıtlamayı bir sorun olarak ekleyeceğim.
NauticalMile

1
@Liza Gotcha'mı oku Bunun neden olduğunu ekledim. Durumunuz için (bunun bu sorudaki gibi olduğunu varsayıyorum ) manuel olarak bir liste oluşturmak xvalsistemiyorsanız, labelLineskodu biraz değiştirmek isteyebilirsiniz : if xvals is None:diğer kriterlere dayalı bir liste oluşturmak için kapsam altındaki kodu değiştirin . xvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
NauticalMile

1
@Liza Grafiğin ilgimi çekiyor. Sorun şu ki, verileriniz arsa boyunca eşit olarak dağılmıyor ve neredeyse birbirinin üzerinde olan birçok eğriniz var. Benim çözümümle birçok durumda etiketleri birbirinden ayırmak çok zor olabilir. Bence en iyi çözüm, arsanızın farklı boş kısımlarında istiflenmiş etiket blokları bulundurmaktır. İki blok yığılmış etiket içeren bir örnek için bu grafiğe bakın (1 etiketli bir blok ve 4'lü diğer blok). Bunu uygulamak oldukça uzun bir çalışma olurdu, bunu gelecekte bir noktada yapabilirim.
NauticalMile

1
Not: Matplotlib 2.0'dan beri .get_axes()ve .get_axis_bgcolor()kullanımdan kaldırıldı. Lütfen .axesve ile değiştirin .get_facecolor().
Jiāgěng

1
Bir başka harika şey , onunla labellinesilgili plt.textveya ax.textonunla ilgili özelliklerin olmasıdır. Fonksiyonda ayarlayabileceğiniz fontsizeve bboxparametreler yapabileceğiniz anlamına gelir labelLines().
tionichm

54

@Jan Kuiken'in cevabı kesinlikle iyi düşünülmüş ve ayrıntılı, ancak bazı uyarılar var:

  • her durumda çalışmıyor
  • makul miktarda ekstra kod gerektirir
  • bir arsadan diğerine önemli ölçüde değişebilir

Çok daha basit bir yaklaşım, her bir grafiğin son noktasını açıklamaktır. Vurgu için nokta daire içine alınabilir. Bu, fazladan bir satırla gerçekleştirilebilir:

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

Kullanmak için bir varyant olacaktır ax.annotate.


1
+1! Güzel ve basit bir çözüm gibi görünüyor. Tembellik için üzgünüm ama bu nasıl görünür? Metin grafiğin içinde mi yoksa sağ y ekseninin üstünde mi?
rocarvaj

1
@rocarvaj Diğer ayarlara bağlıdır. Etiketlerin plot kutusunun dışına çıkması mümkündür. Bu davranışı önlemenin iki yolu şunlardır: 1) farklı bir indeks kullanmak -1, 2) etiketler için alan sağlamak için uygun eksen sınırlarını ayarlamak.
Ioannis Filippidis


@LazyCat: Bu doğru. Bunu düzeltmek için ek açıklamalar sürüklenebilir hale getirilebilir. Sanırım biraz acı ama işe yarayacak.
PlacidLush

1

Ioannis Filippidis'in yaptığı gibi daha basit bir yaklaşım:

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

sageCell üzerinde python 3 kodu

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.