Встроенные метки в 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

3 ответов


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


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


Красивая Картинка:

semi-automatic plot-labeling

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

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

описание

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

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

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

вопросы

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

Gotchas

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

@января 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))

A вариант будет использовать ax.annotate.