ป้ายกำกับแบบอินไลน์ใน Matplotlib


105

ใน Matplotlib ไม่ยากเกินไปที่จะสร้างตำนาน ( example_legend()ด้านล่าง) แต่ฉันคิดว่ามันเป็นสไตล์ที่ดีกว่าที่จะวางป้ายกำกับบนเส้นโค้งที่กำลังพล็อต (ตามexample_inline()ด้านล่าง) นี่อาจเป็นเรื่องยุ่งมากเพราะฉันต้องระบุพิกัดด้วยมือและถ้าฉันจัดรูปแบบพล็อตใหม่ฉันอาจต้องเปลี่ยนตำแหน่งป้ายกำกับ มีวิธีสร้างฉลากบนเส้นโค้งโดยอัตโนมัติใน Matplotlib หรือไม่? คะแนนโบนัสสำหรับความสามารถในการจัดแนวข้อความในมุมที่สอดคล้องกับมุมของเส้นโค้ง

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

รูปที่มีตำนาน

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

รูปที่มีป้ายกำกับแบบอินไลน์

คำตอบ:


30

เป็นคำถามที่ดีเมื่อสักครู่ที่ผ่านมาฉันได้ทดลองกับสิ่งนี้เล็กน้อย แต่ไม่ได้ใช้บ่อยนักเพราะมันยังไม่สามารถกันกระสุนได้ ฉันแบ่งพื้นที่พล็อตออกเป็นตาราง 32x32 และคำนวณ 'ฟิลด์ที่เป็นไปได้' สำหรับตำแหน่งที่ดีที่สุดของป้ายกำกับสำหรับแต่ละบรรทัดตามกฎต่อไปนี้:

  • พื้นที่สีขาวเป็นสถานที่ที่ดีสำหรับฉลาก
  • ฉลากควรอยู่ใกล้กับบรรทัดที่ตรงกัน
  • ป้ายกำกับควรอยู่ห่างจากบรรทัดอื่น ๆ

รหัสเป็นดังนี้:

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

และพล็อตผลลัพธ์: ใส่คำอธิบายภาพที่นี่


ดีมาก. อย่างไรก็ตามฉันมีตัวอย่างที่ใช้งานได้ไม่สมบูรณ์: 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();สิ่งนี้ทำให้หนึ่งในป้ายกำกับที่มุมบนซ้าย มีแนวคิดในการแก้ไขปัญหานี้อย่างไร ดูเหมือนว่าปัญหาอาจเกิดจากเส้นอยู่ใกล้กันเกินไป
egpbos

x2 = np.linspace(0,0.5,100)ขออภัยลืม
egpbos

มีวิธีใดบ้างที่จะใช้สิ่งนี้โดยไม่ต้องใช้ scipy? ในระบบปัจจุบันของฉันมันเป็นความเจ็บปวดในการติดตั้ง
AnnanFay

สิ่งนี้ใช้ไม่ได้สำหรับฉันภายใต้ Python 3.6.4, Matplotlib 2.1.2 และ Scipy 1.0.0 หลังจากอัปเดตprintคำสั่งแล้วคำสั่งจะรันและสร้าง 4 พล็อตซึ่ง 3 ในนั้นดูเหมือนจะเป็นพิกเซลที่พูดพล่อยๆ (อาจเกี่ยวข้องกับ 32x32) และอันที่สี่ที่มีป้ายกำกับในตำแหน่งคี่
วายเดวิส

86

ปรับปรุง:ผู้ใช้cphycได้สร้างความกรุณาที่เก็บ Github รหัสในคำตอบนี้ (ดูที่นี่ ) pip install matplotlib-label-linesและรวมรหัสลงในแพคเกจซึ่งอาจถูกติดตั้งโดยใช้


ภาพสวย:

การติดป้ายพล็อตกึ่งอัตโนมัติ

ในmatplotlibการติดป้ายโครงร่างโครงร่างนั้นค่อนข้างง่าย (โดยอัตโนมัติหรือวางป้ายกำกับด้วยตนเองด้วยการคลิกเมาส์) ดูเหมือนว่าจะไม่มีความสามารถเทียบเท่ากับชุดข้อมูลในรูปแบบนี้! อาจมีเหตุผลเชิงความหมายบางประการที่ไม่รวมคุณลักษณะนี้ซึ่งฉันขาดหายไป

ไม่ว่าฉันจะเขียนโมดูลต่อไปนี้ซึ่งใช้การอนุญาตใด ๆ สำหรับการติดฉลากพล็อตกึ่งอัตโนมัติ ต้องใช้เพียงnumpyสองสามฟังก์ชันจากmathไลบรารีมาตรฐาน

คำอธิบาย

ลักษณะการทำงานเริ่มต้นของlabelLinesฟังก์ชันคือการเว้นวรรคป้ายกำกับให้เท่า ๆ กันตามแนวxแกน (วางตามyค่า - ค่าที่ถูกต้องโดยอัตโนมัติ) หากคุณต้องการคุณสามารถส่งอาร์เรย์ของพิกัด x ของแต่ละป้ายกำกับ คุณยังสามารถปรับแต่งตำแหน่งของป้ายกำกับหนึ่งป้าย (ดังแสดงในพล็อตด้านล่างขวา) และเว้นระยะห่างเท่า ๆ กันหากต้องการ

นอกจากนี้ไฟล์ label_linesฟังก์ชั่นนี้ไม่ได้อธิบายถึงบรรทัดที่ไม่มีเลเบลที่กำหนดไว้ในplotคำสั่ง (หรือถูกต้องมากขึ้นหากมีป้ายกำกับ'_line')

อาร์กิวเมนต์คำหลักถูกส่งไปยัง labelLinesหรือlabelLineถูกส่งต่อไปยังการtextเรียกใช้ฟังก์ชัน (อาร์กิวเมนต์คำสำคัญบางตัวถูกตั้งค่าหากรหัสการเรียกใช้ไม่ระบุ)

ปัญหา

  • บางครั้งกรอบคำอธิบายประกอบจะรบกวนเส้นโค้งอื่น ๆ อย่างไม่น่าพึงปรารถนา ดังที่แสดงโดย110คำอธิบายประกอบและในพล็อตด้านซ้ายบน ฉันไม่แน่ใจว่าสิ่งนี้สามารถหลีกเลี่ยงได้
  • จะเป็นการดีหากระบุก yตำแหน่งแทนในบางครั้ง
  • ยังคงเป็นกระบวนการซ้ำเพื่อรับคำอธิบายประกอบในตำแหน่งที่ถูกต้อง
  • ใช้ได้เฉพาะเมื่อxค่า -axis เป็นfloats

Gotchas

  • โดยค่าเริ่มต้นlabelLinesฟังก์ชันจะถือว่าชุดข้อมูลทั้งหมดครอบคลุมช่วงที่ระบุโดยขีด จำกัด ของแกน ลองดูเส้นโค้งสีน้ำเงินในพล็อตด้านซ้ายบนของภาพสวย ๆ หากมีเฉพาะข้อมูลสำหรับxช่วง0.5-1แล้วเราก็ไม่อาจวางป้ายชื่อที่ตำแหน่งที่ต้องการ (ซึ่งน้อยกว่า0.2) ดูคำถามนี้สำหรับตัวอย่างที่น่ารังเกียจโดยเฉพาะ ในตอนนี้รหัสไม่ได้ระบุสถานการณ์นี้อย่างชาญฉลาดและจัดเรียงป้ายกำกับใหม่อย่างไรก็ตามมีวิธีแก้ปัญหาที่สมเหตุสมผล ฟังก์ชัน labelLines รับxvalsอาร์กิวเมนต์ รายการของ - xค่าที่ผู้ใช้ระบุแทนการแจกแจงเชิงเส้นเริ่มต้นตามความกว้าง ดังนั้นผู้ใช้สามารถตัดสินใจได้ว่าx- ค่าที่จะใช้สำหรับตำแหน่งป้ายกำกับของชุดข้อมูลแต่ละชุด

นอกจากนี้ฉันเชื่อว่านี่เป็นคำตอบแรกที่จะทำให้ไฟล์ บรรลุวัตถุประสงค์โบนัสในการจัดแนวฉลากให้ตรงกับเส้นโค้ง :)

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)

รหัสทดสอบเพื่อสร้างภาพสวย ๆ ด้านบน:

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 ฉันดีใจที่คุณสามารถปรับให้เข้ากับความต้องการของคุณได้ ฉันจะเพิ่มข้อ จำกัด นั้นเป็นปัญหา
NauticalMile

1
@Liza อ่าน Gotcha ของฉันฉันเพิ่งเพิ่มว่าทำไมสิ่งนี้จึงเกิดขึ้น สำหรับกรณีของคุณ (ฉันสมมติว่ามันเหมือนกับคำถามนี้ ) เว้นแต่คุณต้องการสร้างรายการด้วยตนเองxvalsคุณอาจต้องการแก้ไขlabelLinesโค้ดเล็กน้อย: เปลี่ยนรหัสภายใต้if xvals is None:ขอบเขตเพื่อสร้างรายการตามเกณฑ์อื่น ๆ คุณสามารถเริ่มต้นด้วยxvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
NauticalMile

1
@ ลิซ่ากราฟของคุณทำให้ฉันสนใจ ปัญหาคือข้อมูลของคุณไม่ได้กระจายไปทั่วพล็อตอย่างเท่าเทียมกันและคุณมีเส้นโค้งจำนวนมากซึ่งเกือบจะอยู่ด้านบนของกันและกัน ด้วยวิธีแก้ปัญหาของฉันอาจเป็นเรื่องยากมากที่จะแยกฉลากออกจากกันในหลาย ๆ กรณี ฉันคิดว่าวิธีแก้ปัญหาที่ดีที่สุดคือการมีบล็อกของป้ายกำกับแบบเรียงซ้อนในส่วนว่างต่างๆของพล็อตของคุณ ดูกราฟนี้สำหรับตัวอย่างที่มีป้ายกำกับแบบเรียงซ้อนสองบล็อก (บล็อกหนึ่งมี 1 ป้ายกำกับและอีกบล็อกที่มี 4) การใช้สิ่งนี้จะเป็นการทำงานที่ดีพอสมควรฉันอาจจะทำในอนาคต
NauticalMile

1
หมายเหตุ: ตั้งแต่ Matplotlib 2.0 .get_axes()และ.get_axis_bgcolor()เลิกใช้แล้ว กรุณาแทนที่ด้วย.axesและ.get_facecolor()resp.
Jiāgěng

1
สิ่งที่ยอดเยี่ยมอีกอย่างlabellinesคือคุณสมบัติที่เกี่ยวข้องplt.textหรือax.textนำไปใช้กับมัน หมายความว่าคุณสามารถตั้งค่าfontsizeและbboxพารามิเตอร์ในlabelLines()ฟังก์ชันได้
tionichm

55

คำตอบของ @Jan Kuiken คือการไตร่ตรองและถี่ถ้วน แต่มีข้อแม้บางประการ:

  • มันไม่ได้ผลในทุกกรณี
  • มันต้องใช้รหัสพิเศษจำนวนพอสมควร
  • มันอาจแตกต่างกันไปมากในแต่ละพล็อต

วิธีที่ง่ายกว่ามากคือใส่คำอธิบายประกอบจุดสุดท้ายของแต่ละพล็อต จุดนี้ยังสามารถหมุนวนเพื่อเน้น สิ่งนี้สามารถทำได้ด้วยบรรทัดพิเศษหนึ่งบรรทัด:

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

ax.annotateเป็นตัวแปรที่จะใช้


1
+1! ดูเหมือนวิธีแก้ปัญหาที่ดีและเรียบง่าย ขออภัยในความขี้เกียจ แต่นี่จะดูยังไง? ข้อความจะอยู่ในพล็อตหรือด้านบนของแกน y ทางขวา?
rocarvaj

1
@rocarvaj ขึ้นอยู่กับการตั้งค่าอื่น ๆ ฉลากจะยื่นออกมานอกกรอบแผนภาพได้ สองวิธีในการหลีกเลี่ยงพฤติกรรมนี้คือ 1) ใช้ดัชนีที่แตกต่างจาก-12) ตั้งค่าขีด จำกัด แกนที่เหมาะสมเพื่อให้มีพื้นที่สำหรับป้ายชื่อ
Ioannis Filippidis

1
นอกจากนี้ยังกลายเป็นเรื่องยุ่งอีกด้วยหากพล็อตมุ่งเน้นไปที่ค่า y - จุดสิ้นสุดใกล้เกินไปสำหรับข้อความที่จะดูดี
LazyCat

@LazyCat: นั่นคือเรื่องจริง ในการแก้ไขปัญหานี้เราสามารถทำให้คำอธิบายประกอบลากได้ ฉันเดาว่าเจ็บนิดหน่อย แต่มันคงเป็นเคล็ดลับ
PlacidLush

1

แนวทางที่ง่ายกว่าเช่นเดียวกับที่ 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()

รหัส python 3 บน sageCell

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.