Các nhãn nội tuyến trong Matplotlib


100

Trong Matplotlib, không quá khó để tạo ra một huyền thoại ( example_legend(), bên dưới), nhưng tôi nghĩ tốt hơn là đặt nhãn ngay trên các đường cong được vẽ (như trong example_inline(), bên dưới). Điều này có thể rất rắc rối, bởi vì tôi phải xác định tọa độ bằng tay và, nếu tôi định dạng lại cốt truyện, tôi có thể phải định vị lại các nhãn. Có cách nào để tự động tạo nhãn trên các đường cong trong Matplotlib không? Điểm thưởng khi có thể định hướng văn bản theo một góc tương ứng với góc của đường cong.

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

Hình có chú giải

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

Hình có nhãn nội tuyến

Câu trả lời:


28

Câu hỏi hay, cách đây một lúc, tôi đã thử nghiệm một chút với cái này, nhưng chưa sử dụng nó nhiều vì nó vẫn chưa chống đạn. Tôi chia diện tích lô đất thành lưới 32x32 và tính toán 'trường tiềm năng' cho vị trí tốt nhất của nhãn cho mỗi dòng theo các quy tắc sau:

  • khoảng trắng là một nơi tốt cho một nhãn
  • Nhãn phải gần dòng tương ứng
  • Nhãn phải cách xa các dòng khác

Mã như thế này:

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

Và cốt truyện kết quả: nhập mô tả hình ảnh ở đây


Rất đẹp. Tuy nhiên, tôi có một ví dụ không hoàn toàn hoạt động: 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();Điều này đặt một trong các nhãn ở góc trên bên trái. có ý tưởng nào để sửa cái này không? Có vẻ như vấn đề có thể là các dòng quá gần nhau.
egpbos

Xin lỗi, đã quên x2 = np.linspace(0,0.5,100).
egpbos

Có cách nào để sử dụng điều này mà không cần scipy? Trên hệ thống hiện tại của tôi, thật khó cài đặt.
AnnanFay

Điều này không hoạt động với tôi trong Python 3.6.4, Matplotlib 2.1.2 và Scipy 1.0.0. Sau khi cập nhật printlệnh, nó chạy và tạo ra 4 ô, 3 trong số đó có vẻ như là chữ vô nghĩa được pixel hóa (có thể là điều gì đó liên quan đến 32x32) và ô thứ tư có nhãn ở những vị trí kỳ lạ.
Y Davis

84

Cập nhật: Người dùng cphyc đã vui lòng tạo một kho lưu trữ Github cho mã trong câu trả lời này (xem tại đây ) và gói mã vào một gói có thể được cài đặt bằng cách sử dụng pip install matplotlib-label-lines.


Bức tranh đẹp:

dán nhãn bán tự động

Trong matplotlibđó, khá dễ dàng để gắn nhãn các ô đường viền (tự động hoặc bằng cách đặt nhãn theo cách thủ công với các cú nhấp chuột). Dường như chưa (chưa) có bất kỳ khả năng tương đương nào để gắn nhãn chuỗi dữ liệu theo kiểu này! Có thể có một số lý do ngữ nghĩa cho việc không bao gồm tính năng này mà tôi đang thiếu.

Bất kể, tôi đã viết mô-đun sau đây có bất kỳ cho phép ghi nhãn ô bán tự động. Nó chỉ yêu cầu numpyvà một vài chức năng từ maththư viện chuẩn .

Sự miêu tả

Hoạt động mặc định của labelLineshàm là tạo khoảng cách cho các nhãn đồng đều dọc theo xtrục (tự động đặt đúngy dĩ nhiên là trị ). Nếu bạn muốn, bạn có thể chỉ cần chuyển một mảng các tọa độ x của mỗi nhãn. Bạn thậm chí có thể điều chỉnh vị trí của một nhãn (như được hiển thị trong ô dưới cùng bên phải) và tạo khoảng trống cho phần còn lại một cách đồng đều nếu bạn muốn.

Ngoài ra, label_lineschức năng này không tính đến các dòng chưa được gán nhãn trong plotlệnh (hoặc chính xác hơn nếu nhãn có chứa '_line').

Các đối số từ khóa được chuyển đến labelLineshoặc labelLineđược chuyển cho lệnh textgọi hàm (một số đối số từ khóa được đặt nếu mã gọi chọn không chỉ định).

Vấn đề

  • Các hộp giới hạn chú thích đôi khi can thiệp không mong muốn với các đường cong khác. Như được hiển thị bởi 110chú thích trong biểu đồ trên cùng bên trái. Tôi thậm chí không chắc điều này có thể tránh được.
  • Sẽ rất tốt nếu bạn chỉ định một yvị trí thay vì đôi khi.
  • Nó vẫn là một quá trình lặp đi lặp lại để có được các chú thích ở đúng vị trí
  • Nó chỉ hoạt động khi các xgiá trị -axis là floats

Gotchas

  • Theo mặc định, labelLineshàm giả định rằng tất cả các chuỗi dữ liệu nằm trong phạm vi được chỉ định bởi các giới hạn trục. Hãy nhìn vào đường cong màu xanh lam ở ô trên cùng bên trái của bức tranh đẹp. Nếu chỉ có sẵn dữ liệu cho xphạm vi 0.5- 1thì chúng tôi không thể đặt nhãn ở vị trí mong muốn (nhỏ hơn một chút 0.2). Xem câu hỏi này cho một ví dụ đặc biệt khó chịu. Hiện tại, mã không xác định tình huống này một cách thông minh và sắp xếp lại các nhãn, tuy nhiên, có một cách giải quyết hợp lý. Hàm labelLines nhận xvalsđối số; danh sáchx -giá trị do người dùng chỉ định thay vì phân phối tuyến tính mặc định theo chiều rộng. Vì vậy, người dùng có thể quyết địnhx-giá trị để sử dụng cho vị trí nhãn của từng chuỗi dữ liệu.

Ngoài ra, tôi tin rằng đây là câu trả lời đầu tiên để hoàn thành mục tiêu thưởng là căn chỉnh các nhãn với đường cong mà chúng nằm trên. :)

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)

Mã thử nghiệm để tạo ra hình ảnh đẹp ở trên:

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 Tôi rất vui vì bạn đã có thể điều chỉnh nó cho phù hợp với nhu cầu của mình. Tôi sẽ thêm ràng buộc đó như một vấn đề.
NauticalMile

1
@Liza Đọc Gotcha của tôi Tôi vừa thêm vào lý do tại sao điều này lại xảy ra. Đối với trường hợp của bạn (tôi giả sử nó giống như trong câu hỏi này ) trừ khi bạn muốn tạo danh sách theo cách thủ công xvals, bạn có thể muốn sửa đổi labelLinesmã một chút: thay đổi mã trong if xvals is None:phạm vi để tạo danh sách dựa trên các tiêu chí khác. Bạn có thể bắt đầu vớixvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
NauticalMile

1
@Liza Mặc dù vậy, biểu đồ của bạn khiến tôi tò mò. Vấn đề là dữ liệu của bạn không trải đều trên toàn bộ lô đất và bạn có rất nhiều đường cong gần như chồng lên nhau. Với giải pháp của tôi, có thể rất khó để phân biệt các nhãn trong nhiều trường hợp. Tôi nghĩ giải pháp tốt nhất là có các khối nhãn xếp chồng lên nhau ở các phần trống khác nhau trong âm mưu của bạn. Hãy xem biểu đồ này để biết ví dụ với hai khối nhãn xếp chồng lên nhau (một khối có 1 nhãn và khối khác có 4). Thực hiện điều này sẽ là một chút công việc hợp lý, tôi có thể làm điều đó vào một thời điểm nào đó trong tương lai.
NauticalMile

1
Lưu ý: kể từ Matplotlib 2.0, .get_axes().get_axis_bgcolor()đã không được dùng nữa. Vui lòng thay thế bằng .axes.get_facecolor(), res.
Jiāgěng

1
Một điều tuyệt vời khác labellineslà các thuộc tính liên quan đến plt.texthoặc ax.textáp dụng cho nó. Có nghĩa là bạn có thể đặt fontsizebboxcác tham số trong labelLines()hàm.
tionichm

52

Câu trả lời của @Jan Kuiken chắc chắn đã được suy nghĩ kỹ lưỡng và thấu đáo, nhưng có một số lưu ý:

  • nó không hoạt động trong mọi trường hợp
  • nó yêu cầu một lượng mã bổ sung hợp lý
  • nó có thể thay đổi đáng kể từ âm mưu này sang âm mưu tiếp theo

Một cách tiếp cận đơn giản hơn nhiều là chú thích điểm cuối cùng của mỗi âm mưu. Điểm cũng có thể được khoanh tròn để nhấn mạnh. Điều này có thể được thực hiện với một dòng bổ sung:

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

Một biến thể sẽ được sử dụng ax.annotate.


1
+1! Nó trông giống như một giải pháp tốt và đơn giản. Xin lỗi vì sự lười biếng, nhưng điều này sẽ trông như thế nào? Văn bản sẽ nằm bên trong âm mưu hay ở trên cùng của trục y bên phải?
rocarvaj

1
@rocarvaj Nó phụ thuộc vào các cài đặt khác. Có thể để các nhãn nhô ra bên ngoài ô ô. Hai cách để tránh hành vi này là: 1) sử dụng chỉ mục khác với -1, 2) đặt giới hạn trục thích hợp để cho phép không gian cho các nhãn.
Ioannis Filippidis 14/09/17

1
Nó cũng trở thành một mớ hỗn độn, nếu âm mưu tập trung vào một số giá trị y - thiết bị đầu cuối trở nên quá chặt chẽ cho các văn bản để nhìn đẹp
LazyCat

@LazyCat: Đúng là như vậy. Để khắc phục điều này, người ta có thể làm cho các chú thích có thể kéo được. Tôi đoán là hơi đau nhưng nó sẽ thành công.
PlacidLush

1

Một cách tiếp cận đơn giản hơn như cách mà Ioannis Filippidis là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()

code python 3 trên sageCell

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.