¿Es posible que aparezcan etiquetas al pasar el mouse sobre un punto en matplotlib?


146

Estoy usando matplotlib para hacer diagramas de dispersión. Cada punto en el diagrama de dispersión está asociado con un objeto con nombre. Me gustaría poder ver el nombre de un objeto cuando pase el cursor sobre el punto en el diagrama de dispersión asociado con ese objeto. En particular, sería bueno poder ver rápidamente los nombres de los puntos que son atípicos. Lo más cercano que pude encontrar mientras buscaba aquí es el comando de anotación, pero parece crear una etiqueta fija en la trama. Desafortunadamente, con la cantidad de puntos que tengo, el diagrama de dispersión sería ilegible si etiquetara cada punto. ¿Alguien sabe de una manera de crear etiquetas que solo aparecen cuando el cursor se encuentra cerca de ese punto?


2
Las personas que terminan aquí a través de la búsqueda también pueden querer verificar esta respuesta , que es bastante compleja, pero puede ser adecuada según los requisitos.
ImportanceOfBeingErnest

Respuestas:


133

Parece que ninguna de las otras respuestas aquí realmente responde a la pregunta. Entonces, aquí hay un código que usa una dispersión y muestra una anotación al pasar el mouse sobre los puntos de dispersión.

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

ingrese la descripción de la imagen aquí

Debido a que las personas también quieren usar esta solución para una línea en plotlugar de una dispersión, la siguiente sería la misma solución plot(que funciona de manera ligeramente diferente).

En caso de que alguien esté buscando una solución para líneas en ejes gemelos, consulte ¿Cómo hacer que las etiquetas aparezcan al pasar el cursor sobre un punto en varios ejes?

En caso de que alguien esté buscando una solución para gráficos de barras, consulte, por ejemplo, esta respuesta .


1
¡Muy agradable! Una nota, noté que en ind["ind"]realidad es una lista de índices para todos los puntos debajo del cursor. Esto significa que el código anterior realmente le da acceso a todos los puntos en una posición determinada, y no solo al punto más alto. Por ejemplo, si tiene dos puntos superpuestos, el texto podría leerse 1 2, B Co incluso 1 2 3, B C Dsi tuviera 3 puntos superpuestos.
Jvinniec

@Jvinniec Exactamente, hay deliberadamente uno de esos casos en la gráfica anterior (el punto verde y rojo en x ~ 0.4). Si lo pasa, se mostrará 0 8, A I(vea la imagen ).
ImportanceOfBeingErnest

@ImportanceOfBeingErnest este es un gran código, pero cuando se desplaza y se mueve sobre un punto, llama fig.canvas.draw_idle()muchas veces (incluso cambia el cursor a inactivo). Lo resolví almacenando el índice anterior y comprobando si ind["ind"][0] == prev_ind. Luego, solo actualice si se mueve de un punto a otro (texto de actualización), deje de desplazarse (haga invisible la anotación) o comience a desplazarse (haga visible la anotación). Con este cambio, es mucho más limpio y eficiente.
Sembei Norimaki

3
@ Konstantin Sí, esta solución funcionará cuando se utilice %matplotlib notebooken un portátil IPython / Jupyter.
ImportanceOfBeingErnest

1
@OriolAbril (y todos los demás), si tiene un problema que surgió al modificar el código de esta respuesta, haga una pregunta al respecto, enlace a esta respuesta y muestre el código que ha intentado. No tengo forma de saber qué está mal con cada uno de sus códigos sin verlo realmente.
ImportanceOfBeingErnest

66

Esta solución funciona al pasar el cursor sobre una línea sin necesidad de hacer clic en ella:

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

1
Muy útil + 1ed. Probablemente necesite 'eliminar' esto porque el motion_notify_event se repetirá para el movimiento dentro del área de la curva. Simplemente comprobar que el objeto de curva es igual a la curva anterior parece funcionar.
bvanlew

55
Hmm, esto no funcionó de fábrica para mí (tan pocas cosas hacen con matplotlib...), ¿funciona con ipython/ jupyternotebooks? ¿Funciona también cuando hay múltiples subtramas? ¿Qué pasa con un gráfico de barras en lugar de un gráfico de líneas?
dwanderson el

12
Esto imprime la etiqueta en la consola cuando se desplaza. ¿Qué hay de hacer que la etiqueta aparezca en la imagen al pasar el mouse? Entendí que esa era la pregunta.
Nikana Reklawyks

@mbernasocchi muchas gracias, ¿qué necesito alimentar en el argumento gid si quiero ver un histograma (uno diferente para cada punto en la dispersión) o, mejor aún, un mapa de calor de un histograma 2D?
Amitai

@NikanaReklawyks Agregué una respuesta que realmente responde la pregunta.
ImportanceOfBeingErnest

37

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

Esto hace justo lo que necesito, ¡gracias! Como beneficio adicional, para implementarlo, reescribí mi programa para que en lugar de crear dos diagramas de dispersión separados en diferentes colores en la misma figura para representar dos conjuntos de datos, copié el método del ejemplo para asignar color a un punto. Esto hizo que mi programa fuera un poco más fácil de leer y menos código. ¡Ahora a buscar una guía para convertir un color en un número!
jdmcbr

1
Esto es para diagramas de dispersión. ¿Qué pasa con los gráficos de líneas? Traté de hacer que funcione en ellos, pero no lo hace. ¿Hay una solución alternativa?
Sohaib

@Sohaib Vea mi respuesta
texasflood

Tengo una pregunta sobre esto. Cuando disperso mis puntos de esta manera: plt.scatter (X_reduced [y == i, 0], X_reduced [y == i, 1], c = c, label = target_name, picker = True) con un zip para i, c y target_name, ¿está desordenado el orden de mis índices? ¿Y ya no puedo mirar hacia arriba a qué punto de datos pertenece?
Chris

Esto no parece funcionar para las notebooks jupyter 5 con ipython 5. ¿Hay alguna manera fácil de solucionarlo? La printdeclaración también debe usar parens para compatibilidad con python 3
nealmcb

14

Una pequeña edición de un ejemplo proporcionado en 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()

Esto traza una trama en línea recta, como Sohaib preguntaba


5

mpld3 resuélvelo por mí. EDITAR (CÓDIGO AGREGADO):

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

Puedes consultar este ejemplo


Incluya código de muestra y no solo enlace a fuentes externas sin contexto o información. Consulte el Centro de ayuda para obtener más información.
Joseph Farah

55
desafortunadamente mpld3 ya no se mantiene activamente a partir de julio de 2017
Ben Lindsay

El ejemplo de código falla con a TypeError: array([1.]) is not JSON serializable.
P-Gn

@ P-Gn solo sigue el truco aquí stackoverflow.com/questions/48015030/mpld3-with-python-error MPLD3 es una solución simple para esto y una vez que se sigue la respuesta anterior, funciona.
Zalakain

1
@Zalakain Desafortunadamente, mpl3d parece ser abandonado .
P-Gn

5

mplcursors funcionó para mí. mplcursors proporciona anotaciones en las que se puede hacer clic para matplotlib. Está fuertemente inspirado en mpldatacursor ( https://github.com/joferkington/mpldatacursor ), con una API mucho más 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()

Lo uso yo mismo, con mucho, la solución más fácil para alguien que tiene prisa. Acabo de trazar 70 etiquetas y matplotlibhace que cada décima línea sea del mismo color, una pena. mplcursorssin embargo lo resuelve.
ajsp

5

Las otras respuestas no respondieron a mi necesidad de mostrar correctamente información sobre herramientas en una versión reciente de la figura de Jupyter en línea matplotlib. Sin embargo, este funciona:

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

Llevando a algo como la siguiente imagen al pasar sobre un punto con el mouse: ingrese la descripción de la imagen aquí



No pude hacer que esto funcione en el laboratorio de jupyter. ¿Tal vez funciona en un cuaderno jupyter pero no en el laboratorio jupyter?
MD004

3

Si usa jupyter notebook, mi solución es tan simple como:

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

Puedes conseguir algo como ingrese la descripción de la imagen aquí


Con mucho, la mejor solución, solo unas pocas líneas de código hacen exactamente lo que OP pidió
Tim Johnsen,

0

He creado un sistema de anotación de varias líneas para agregar a: https://stackoverflow.com/a/47166787/10302020 . para la versión más actualizada: https://github.com/AidenBurgess/MultiAnnotationLineGraph

Simplemente cambie los datos en la sección 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()
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.