É possível fazer com que os rótulos apareçam ao passar o mouse sobre um ponto no matplotlib?

146

Estou usando o matplotlib para fazer gráficos de dispersão. Cada ponto no gráfico de dispersão é associado a um objeto nomeado. Gostaria de poder ver o nome de um objeto quando passar o cursor sobre o ponto no gráfico de dispersão associado a esse objeto. Em particular, seria bom poder ver rapidamente os nomes dos pontos discrepantes. A coisa mais próxima que consegui encontrar durante a pesquisa aqui é o comando de anotação, mas isso parece criar um rótulo fixo no gráfico. Infelizmente, com o número de pontos que tenho, o gráfico de dispersão seria ilegível se eu rotulasse cada ponto. Alguém sabe como criar rótulos que só aparecem quando o cursor passa o mouse nas proximidades desse ponto?

jdmcbr
fonte
2
As pessoas que terminam aqui por meio da pesquisa também podem querer verificar essa resposta , que é bastante complexa, mas pode ser adequada, dependendo dos requisitos.
ImportanceOfBeingErnest

Respostas:

133

Parece que nenhuma das outras respostas aqui realmente responde à pergunta. Então, aqui está um código que usa uma dispersão e mostra uma anotação ao passar o mouse sobre os pontos de dispersão.

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(1)

x = np.random.rand(15)
y = np.random.rand(15)
names = np.array(list("ABCDEFGHIJKLMNO"))
c = np.random.randint(1,5,size=15)

norm = plt.Normalize(1,4)
cmap = plt.cm.RdYlGn

fig,ax = plt.subplots()
sc = plt.scatter(x,y,c=c, s=100, cmap=cmap, norm=norm)

annot = ax.annotate("", xy=(0,0), xytext=(20,20),textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="w"),
                    arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)

def update_annot(ind):

    pos = sc.get_offsets()[ind["ind"][0]]
    annot.xy = pos
    text = "{}, {}".format(" ".join(list(map(str,ind["ind"]))), 
                           " ".join([names[n] for n in ind["ind"]]))
    annot.set_text(text)
    annot.get_bbox_patch().set_facecolor(cmap(norm(c[ind["ind"][0]])))
    annot.get_bbox_patch().set_alpha(0.4)


def hover(event):
    vis = annot.get_visible()
    if event.inaxes == ax:
        cont, ind = sc.contains(event)
        if cont:
            update_annot(ind)
            annot.set_visible(True)
            fig.canvas.draw_idle()
        else:
            if vis:
                annot.set_visible(False)
                fig.canvas.draw_idle()

fig.canvas.mpl_connect("motion_notify_event", hover)

plt.show()

insira a descrição da imagem aqui

Como as pessoas também querem usar esta solução para uma linha em plotvez de uma dispersão, a seguinte seria a mesma solução para plot(que funciona de maneira um pouco diferente).

Caso alguém esteja procurando uma solução para linhas em eixos duplos, consulte Como fazer etiquetas aparecerem ao passar o mouse sobre um ponto em vários eixos?

Caso alguém esteja procurando uma solução para gráficos de barras, consulte, por exemplo, esta resposta .

ImportanceOfBeingErnest
fonte
1
Muito agradável! Uma observação, notei que ind["ind"]na verdade é uma lista de índices para todos os pontos sob o cursor. Isso significa que o código acima realmente oferece acesso a todos os pontos em uma determinada posição, e não apenas ao ponto mais alto. Por exemplo, se você tiver dois pontos sobrepostos, o texto poderá ser lido 1 2, B Cou mesmo 1 2 3, B C Dse você tiver três pontos sobrepostos.
Jvinniec
@Jvinniec Exatamente, existe deliberadamente um desses casos no gráfico acima (o ponto verde e vermelho em x ~ 0,4). Se você passar o mouse, ele será exibido 0 8, A I(veja a figura ).
ImportanceOfBeingErnest
@ImportanceOfBeingErnest, esse é um ótimo código, mas ao passar o mouse e mover-se sobre um ponto, ele chama fig.canvas.draw_idle()muitas vezes (até muda o cursor para inativo). Eu o resolvi armazenando o índice anterior e verificando se ind["ind"][0] == prev_ind. Atualize somente se você mover de um ponto para outro (atualizar texto), parar de passar o mouse (tornar a anotação invisível) ou começar a passar o mouse (tornar a anotação visível). Com essa mudança, é muito mais limpo e eficiente.
Sembei Norimaki
3
@ Konstantin Sim, esta solução funcionará ao ser usada %matplotlib notebookem um notebook IPython / Jupyter.
ImportanceOfBeingErnest
1
@OriolAbril (e todos os outros), se você tiver um problema ao modificar o código desta resposta, faça uma pergunta sobre ele, vincule-o a esta resposta e mostre o código que você tentou. Não tenho como saber o que há de errado com cada um dos seus códigos sem realmente vê-lo.
ImportanceOfBeingErnest
66

Esta solução funciona ao passar o mouse sobre uma linha sem a necessidade de clicar nela:

import matplotlib.pyplot as plt

# Need to create as global variable so our callback(on_plot_hover) can access
fig = plt.figure()
plot = fig.add_subplot(111)

# create some curves
for i in range(4):
    # Giving unique ids to each data member
    plot.plot(
        [i*1,i*2,i*3,i*4],
        gid=i)

def on_plot_hover(event):
    # Iterating over each data member plotted
    for curve in plot.get_lines():
        # Searching which data member corresponds to current mouse position
        if curve.contains(event)[0]:
            print "over %s" % curve.get_gid()

fig.canvas.mpl_connect('motion_notify_event', on_plot_hover)           
plt.show()
mbernasocchi
fonte
1
Muito útil + 1ed. Você provavelmente precisará 'rebater' isso porque o motion_notify_event será repetido para o movimento dentro da área da curva. Simplesmente verificar se o objeto da curva é igual à curva anterior parece funcionar.
bvanlew
5
Hmm - isso não funcionou pronto para mim (tão poucas coisas funcionam com matplotlib...) - isso funciona com ipython/ jupyternotebooks? Também funciona quando há várias subparcelas? O que dizer de um gráfico de barras em vez de um gráfico de linhas?
precisa saber é o seguinte
12
Isso imprime a etiqueta no console ao passar o mouse. Que tal fazer com que o rótulo apareça na foto ao passar o mouse? Eu entendi que essa era a pergunta.
Nikana Reklawyks
@mbernasocchi muito obrigado, o que eu preciso para alimentar o argumento gid se eu quiser ver um histograma (um diferente para cada ponto da dispersão) ou, melhor ainda, um mapa de calor de um histograma 2D?
Amitai
@NikanaReklawyks Adicionei uma resposta que realmente responde à pergunta.
ImportanceOfBeingErnest
37

Em http://matplotlib.sourceforge.net/examples/event_handling/pick_event_demo.html :

from matplotlib.pyplot import figure, show
import numpy as npy
from numpy.random import rand


if 1: # picking on a scatter plot (matplotlib.collections.RegularPolyCollection)

    x, y, c, s = rand(4, 100)
    def onpick3(event):
        ind = event.ind
        print('onpick3 scatter:', ind, npy.take(x, ind), npy.take(y, ind))

    fig = figure()
    ax1 = fig.add_subplot(111)
    col = ax1.scatter(x, y, 100*s, c, picker=True)
    #fig.savefig('pscoll.eps')
    fig.canvas.mpl_connect('pick_event', onpick3)

show()
cyborg
fonte
Isso faz exatamente o que eu preciso, obrigado! Como bônus, para implementá-lo, reescrevi meu programa para que, em vez de criar dois gráficos de dispersão separados em cores diferentes na mesma figura para representar dois conjuntos de dados, copiei o método do exemplo para atribuir cores a um ponto. Isso tornou meu programa um pouco mais simples de ler e menos código. Agora, descubra um guia para converter uma cor em um número!
Jdmcbr 27/10/11
1
Isto é para gráficos de dispersão. E as plotagens de linha? Tentei fazê-lo funcionar neles, mas não funciona. Existe uma solução alternativa?
Sohaib
@Sohaib Veja minha resposta
texasflood /
Eu tenho uma pergunta sobre isso. Quando eu disperso os meus pontos da seguinte maneira: plt.scatter (X_reduced [y == i, 0], X_reduced [y == i, 1], c = c, label = target_name, picker = True) com um zip para i, ce target_name, a ordem dos meus índices está desarrumada? E não consigo mais procurar a que ponto de dados pertence?
Chris
Isso não parece funcionar para os notebooks jupyter 5 com ipython 5. Existe uma maneira fácil de corrigir isso? A printdeclaração também deve usar parênteses para compatibilidade com python 3
nealmcb
14

Uma ligeira edição em um exemplo fornecido em http://matplotlib.org/users/shell.html :

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('click on points')

line, = ax.plot(np.random.rand(100), '-', picker=5)  # 5 points tolerance


def onpick(event):
    thisline = event.artist
    xdata = thisline.get_xdata()
    ydata = thisline.get_ydata()
    ind = event.ind
    print('onpick points:', *zip(xdata[ind], ydata[ind]))


fig.canvas.mpl_connect('pick_event', onpick)

plt.show()

Isso traça uma trama em linha reta, como Sohaib estava perguntando

texasflood
fonte
5

mpld3 resolva isso para mim. EDITAR (CÓDIGO ADICIONADO):

import matplotlib.pyplot as plt
import numpy as np
import mpld3

fig, ax = plt.subplots(subplot_kw=dict(axisbg='#EEEEEE'))
N = 100

scatter = ax.scatter(np.random.normal(size=N),
                 np.random.normal(size=N),
                 c=np.random.random(size=N),
                 s=1000 * np.random.random(size=N),
                 alpha=0.3,
                 cmap=plt.cm.jet)
ax.grid(color='white', linestyle='solid')

ax.set_title("Scatter Plot (with tooltips!)", size=20)

labels = ['point {0}'.format(i + 1) for i in range(N)]
tooltip = mpld3.plugins.PointLabelTooltip(scatter, labels=labels)
mpld3.plugins.connect(fig, tooltip)

mpld3.show()

Você pode verificar este exemplo

Julian
fonte
Inclua um código de exemplo e não apenas vincule a fontes externas sem contexto ou informações. Consulte a Central de Ajuda para mais informações.
Joseph Farah
5
infelizmente o mpld3 não está mais sendo mantido ativamente a partir de julho de 2017
Ben Lindsay
A amostra de código falha com a TypeError: array([1.]) is not JSON serializable.
P-Gn
@ P-Gn basta seguir o truque aqui stackoverflow.com/questions/48015030/mpld3-with-python-error MPLD3 é uma solução simples para isso e, uma vez que a resposta acima é seguida, ela funciona.
Zalakain
1
@Zalakain Infelizmente, o mpl3d parece ter sido abandonado .
P-Gn
5

mplcursors trabalhou para mim. O mplcursors fornece anotação clicável para o matplotlib. É fortemente inspirado no mpldatacursor ( https://github.com/joferkington/mpldatacursor ), com uma API muito simplificada

import matplotlib.pyplot as plt
import numpy as np
import mplcursors

data = np.outer(range(10), range(1, 5))

fig, ax = plt.subplots()
lines = ax.plot(data)
ax.set_title("Click somewhere on a line.\nRight-click to deselect.\n"
             "Annotations can be dragged.")

mplcursors.cursor(lines) # or just mplcursors.cursor()

plt.show()
Enayat
fonte
Eu mesmo uso isso, de longe a solução mais fácil para alguém com pressa. Acabei de plotar 70 etiquetas e matplotlibtornar a cada 10 linhas a mesma cor, uma dor. mplcursorsresolve isso embora.
ajsp 6/05/19
5

As outras respostas não abordaram minha necessidade de mostrar corretamente as dicas de ferramentas em uma versão recente da figura in-line do matupotlib do Jupyter. Este funciona embora:

import matplotlib.pyplot as plt
import numpy as np
import mplcursors
np.random.seed(42)

fig, ax = plt.subplots()
ax.scatter(*np.random.random((2, 26)))
ax.set_title("Mouse over a point")
crs = mplcursors.cursor(ax,hover=True)

crs.connect("add", lambda sel: sel.annotation.set_text(
    'Point {},{}'.format(sel.target[0], sel.target[1])))
plt.show()

Levando a algo como a seguinte imagem ao passar por cima de um ponto com o mouse: insira a descrição da imagem aqui

Farzad Vertigo
fonte
3
A fonte para isso (sem atribuição) é mplcursors.readthedocs.io/en/stable/examples/hover.html
Victoria Stuart
Não consegui fazer isso funcionar no jupyter lab. Talvez ele funcione em um notebook jupyter, mas não no laboratório jupyter?
MD004 23/01
3

Se você usa o notebook jupyter, minha solução é simples:

%pylab
import matplotlib.pyplot as plt
import mplcursors
plt.plot(...)
mplcursors.cursor(hover=True)
plt.show()

Você pode obter algo como insira a descrição da imagem aqui

Yuchao Jiang
fonte
De longe a melhor solução, apenas algumas linhas de código fazem exatamente o que o OP pediu
Tim Johnsen
0

Eu criei um sistema de anotação de várias linhas para adicionar a: https://stackoverflow.com/a/47166787/10302020 . para a versão mais atualizada: https://github.com/AidenBurgess/MultiAnnotationLineGraph

Simplesmente altere os dados na seção inferior.

import matplotlib.pyplot as plt


def update_annot(ind, line, annot, ydata):
    x, y = line.get_data()
    annot.xy = (x[ind["ind"][0]], y[ind["ind"][0]])
    # Get x and y values, then format them to be displayed
    x_values = " ".join(list(map(str, ind["ind"])))
    y_values = " ".join(str(ydata[n]) for n in ind["ind"])
    text = "{}, {}".format(x_values, y_values)
    annot.set_text(text)
    annot.get_bbox_patch().set_alpha(0.4)


def hover(event, line_info):
    line, annot, ydata = line_info
    vis = annot.get_visible()
    if event.inaxes == ax:
        # Draw annotations if cursor in right position
        cont, ind = line.contains(event)
        if cont:
            update_annot(ind, line, annot, ydata)
            annot.set_visible(True)
            fig.canvas.draw_idle()
        else:
            # Don't draw annotations
            if vis:
                annot.set_visible(False)
                fig.canvas.draw_idle()


def plot_line(x, y):
    line, = plt.plot(x, y, marker="o")
    # Annotation style may be changed here
    annot = ax.annotate("", xy=(0, 0), xytext=(-20, 20), textcoords="offset points",
                        bbox=dict(boxstyle="round", fc="w"),
                        arrowprops=dict(arrowstyle="->"))
    annot.set_visible(False)
    line_info = [line, annot, y]
    fig.canvas.mpl_connect("motion_notify_event",
                           lambda event: hover(event, line_info))


# Your data values to plot
x1 = range(21)
y1 = range(0, 21)
x2 = range(21)
y2 = range(0, 42, 2)
# Plot line graphs
fig, ax = plt.subplots()
plot_line(x1, y1)
plot_line(x2, y2)
plt.show()
Bobs
fonte