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

103

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

Рисунок со встроенными метками

Алекс Сатмари
источник

Ответы:

29

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

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


Симпатичная картинка:

полуавтоматическая маркировка участков

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

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

Описание

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

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

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

вопросы

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

Попался

  • По умолчанию 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
@Liza Но твой график меня заинтриговал. Проблема в том, что ваши данные неравномерно распределены по графику, и у вас есть много кривых, которые почти накладываются друг на друга. С моим решением во многих случаях может быть очень трудно отличить метки друг от друга. Я думаю, что лучшим решением будет иметь блоки наклеек в разных пустых частях вашего участка. См. Этот график для примера с двумя блоками сгруппированных этикеток (один блок с 1 меткой, а другой блок с 4). Реализация этого была бы изрядной легкостью, я мог бы сделать это в какой-то момент в будущем.
NauticalMile
1
Примечание: так как Matplotlib 2.0, .get_axes()и .get_axis_bgcolor()устарели. Пожалуйста, замените на .axesи .get_facecolor(), соответственно.
Jiāgěng
1
Еще одна замечательная вещь labellines- это свойства, связанные с ним plt.textили ax.textприменяемые к нему. Значение можно установить fontsizeи bboxпараметры в labelLines()функции.
тионичм 04
53

Ответ @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) использовать индекс, отличный от -1, 2) установить соответствующие пределы оси, чтобы оставить место для меток.
Иоаннис Филиппидис
1
Также становится беспорядком, если графики концентрируются на некотором значении y - конечные точки становятся слишком близкими, чтобы текст выглядел красиво
LazyCat 08
@LazyCat: Это правда. Чтобы исправить это, можно сделать аннотации перетаскиваемыми. Думаю, немного неудобно, но это поможет.
PlacidLush
1

Более простой подход, такой как у Иоанниса Филиппидиса:

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

Tagada
источник