Подтвердить что ты не робот

Встроенные ярлыки в Matplotlib

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

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

Figure with inline labels

4b9b3361

Ответ 1

Хороший вопрос, некоторое время назад я немного экспериментировал с этим, но не использовал его много, потому что он все еще не пуленепробиваемый. Я разделил область графика на сетку 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()

И получившийся сюжет: enter image description here

Ответ 2

Обновление: пользователь cphyc любезно создал Github-репозиторий для кода в этом ответе (см. Здесь) и собрал код в пакет, который можно установить с помощью pip install matplotlib-label-lines.


Приятная картина:

semi-automatic plot-labeling

В matplotlib довольно легко маркировать контурные графики (либо автоматически, либо вручную размещая метки щелчками мыши). Похоже, что (пока) не существует какой-либо эквивалентной возможности обозначать серии данных таким образом! Может быть некоторая семантическая причина не включать эту функцию, которую я пропускаю.

Несмотря на это, я написал следующий модуль, который принимает любой позволяет полуавтоматическую маркировку графика. Требуется только numpy и пара функций из стандартной math библиотеки.

Описание

Поведение функции labelLines по labelLines состоит в том, чтобы равномерно размещать метки вдоль оси x (разумеется, автоматически помещая в правильный y -value). Если вы хотите, вы можете просто передать массив координат X каждой из меток. Вы можете даже настроить расположение одной метки (как показано на нижнем правом графике) и равномерно распределить остальные, если хотите.

Кроме того, функция label_lines не учитывает строки, для которых не была назначена метка в команде plot (или, точнее, если метка содержит '_line').

Аргументы ключевых слов, передаваемые в labelLines или labelLine, передаются вызову text функции (некоторые аргументы ключевого слова устанавливаются, если вызывающий код решает не указывать).

вопросы

  • Ограничивающие рамки аннотации иногда нежелательно мешают другим кривым. Как показано 1 и 10 аннотации в верхнем левом графике. Я даже не уверен, что этого можно избежать.
  • Было бы неплохо иногда указывать позицию y.
  • Это все еще итеративный процесс, чтобы получить аннотации в нужном месте
  • Работает только тогда, когда значения x -axis являются числами с float

Gotchas

  • По умолчанию функция labelLines предполагает, что все серии данных охватывают диапазон, заданный пределами оси. Взгляните на синюю кривую на верхнем левом графике красивой картинки. Если бы были доступны только данные для диапазона x 0.5 - 1 то мы не смогли бы разместить метку в желаемом месте (которое чуть меньше 0.2). Смотрите этот вопрос для особенно неприятного примера. В настоящее время код не определяет разумно этот сценарий и не переупорядочивает метки, однако существует разумный обходной путь. Функция labelLines принимает аргумент xvals; список x -value, указанных пользователем вместо линейного распределения по ширине по умолчанию. Таким образом, пользователь может решить, какие x -value использовать для размещения меток в каждой серии данных.

Кроме того, я считаю, что это первый ответ, который завершает бонусную задачу по выравниванию меток с кривой, на которой они находятся. :)

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

Ответ 3

Ответ @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.