Est-il possible de faire apparaître des étiquettes lors du survol d'un point dans matplotlib?


148

J'utilise matplotlib pour créer des nuages ​​de points. Chaque point du nuage de points est associé à un objet nommé. J'aimerais pouvoir voir le nom d'un objet lorsque je place mon curseur sur le point du nuage de points associé à cet objet. En particulier, ce serait bien de pouvoir voir rapidement les noms des points qui sont des valeurs aberrantes. La chose la plus proche que j'ai pu trouver en cherchant ici est la commande annoter, mais cela semble créer une étiquette fixe sur le tracé. Malheureusement, avec le nombre de points que j'ai, le nuage de points serait illisible si j'étiquais chaque point. Quelqu'un connaît-il un moyen de créer des étiquettes qui n'apparaissent que lorsque le curseur survole à proximité de ce point?


2
Les personnes qui se retrouvent ici par la recherche peuvent également vouloir vérifier cette réponse , qui est plutôt complexe, mais peut convenir en fonction des besoins.
ImportanceOfBeingErnest

Réponses:


133

Il semble qu'aucune des autres réponses ici ne réponde réellement à la question. Donc , est un code ici qui utilise une dispersion et montre une annotation sur planant au- dessus des points de dispersion.

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

entrez la description de l'image ici

Parce que les gens veulent également utiliser cette solution pour une ligne plotau lieu d'un nuage de points, ce qui suit serait la même solution pour plot(ce qui fonctionne légèrement différemment).

Dans le cas où quelqu'un recherche une solution pour les lignes dans des axes doubles, reportez-vous à Comment faire apparaître des étiquettes lors du survol d'un point dans plusieurs axes?

Dans le cas où quelqu'un cherche une solution pour les graphiques à barres, veuillez vous référer par exemple à cette réponse .


1
Très agréable! Une note, je remarque que ind["ind"]est en fait une liste des index pour tous les points sous le curseur. Cela signifie que le code ci-dessus vous donne en fait accès à tous les points à une position donnée, et pas seulement au point le plus haut. Par exemple, si vous avez deux points qui se chevauchent, le texte pourrait lire 1 2, B Cou même 1 2 3, B C Dsi vous aviez 3 points qui se chevauchent.
Jvinniec

@Jvinniec Exactement, il y a délibérément un tel cas dans le graphique ci-dessus (le point vert et rouge à x ~ 0,4). Si vous le survolez, il s'affichera 0 8, A I(voir image ).
ImportanceOfBeingErnest

@ImportanceOfBeingErnest c'est un excellent code, mais en survolant et en se déplaçant sur un point, il appelle fig.canvas.draw_idle()plusieurs fois (il change même le curseur en inactif). Je l'ai résolu en stockant l'index précédent et en vérifiant si ind["ind"][0] == prev_ind. Ensuite, ne mettez à jour que si vous vous déplacez d'un point à un autre (mettez à jour le texte), arrêtez le survol (rendez l'annotation invisible) ou commencez le survol (rendez l'annotation visible). Avec ce changement, c'est beaucoup plus propre et efficace.
Sembei Norimaki

3
@Konstantin Oui, cette solution fonctionnera lors de l'utilisation %matplotlib notebookdans un notebook IPython / Jupyter.
ImportanceOfBeingErnest

1
@OriolAbril (et tout le monde), Si vous rencontrez un problème lors de la modification du code de cette réponse, posez une question à ce sujet, créez un lien vers cette réponse et affichez le code que vous avez tenté. Je n'ai aucun moyen de savoir ce qui ne va pas avec chacun de vos codes sans le voir réellement.
ImportanceOfBeingErnest

66

Cette solution fonctionne lors du survol d'une ligne sans qu'il soit nécessaire de cliquer dessus:

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
Très utile + 1ed. Vous aurez probablement besoin de «anti-rebond» car l'événement motion_notify_event se répétera pour le mouvement à l'intérieur de la zone de la courbe. Le simple fait de vérifier que l'objet courbe est égal à la courbe précédente semble fonctionner.
bvanlew

5
Hmm - cela n'a pas fonctionné directement pour moi (si peu de choses font avec matplotlib...) - est-ce que cela fonctionne avec ipython/ jupyternotebooks? Cela fonctionne-t-il également lorsqu'il y a plusieurs sous-parcelles? Qu'en est-il sur un graphique à barres plutôt qu'un graphique linéaire?
dwanderson

12
Cela imprime l'étiquette dans la console lors du survol. Qu'en est-il de faire apparaître l'étiquette sur l'image lors du survol? J'ai compris que c'était la question.
Nikana Reklawyks

@mbernasocchi merci beaucoup, de quoi ai-je besoin pour alimenter l'argument gid si je veux voir un histogramme (un différent pour chaque point du scatter) ou, mieux encore, une carte thermique d'un histogramme 2D?
Amitai

@NikanaReklawyks J'ai ajouté une réponse qui répond en fait à la question.
ImportanceOfBeingErnest

37

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

Cela fait exactement ce dont j'ai besoin, merci! En prime, afin de l'implémenter, j'ai réécrit mon programme afin qu'au lieu de créer deux nuages ​​de points séparés de couleurs différentes sur la même figure pour représenter deux ensembles de données, j'ai copié la méthode de l'exemple pour attribuer une couleur à un point. Cela a rendu mon programme un peu plus simple à lire et moins de code. Maintenant, c'est parti pour trouver un guide pour convertir une couleur en nombre!
jdmcbr

1
C'est pour les nuages ​​de points. Qu'en est-il des tracés linéaires? J'ai essayé de le faire fonctionner sur eux, mais ce n'est pas le cas. Existe-t-il une solution de contournement?
Sohaib

@Sohaib Voir ma réponse
texasflood

J'ai une question à ce sujet. Quand je disperse mes points comme ceci: plt.scatter (X_reduced [y == i, 0], X_reduced [y == i, 1], c = c, label = target_name, picker = True) avec un zip pour i, c et nom_cible, est-ce que l'ordre de mes index est alors perturbé? Et je ne peux plus chercher à quel point de données il appartient?
Chris

Cela ne semble pas fonctionner pour les notebooks jupyter 5 avec ipython 5. Y a-t-il un moyen simple de résoudre ce problème? L' printinstruction devrait également utiliser des parens pour la compatibilité avec python 3
nealmcb

14

Une légère modification sur un exemple fourni dans 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()

Cela trace un tracé en ligne droite, comme Sohaib le demandait


5

mpld3 résout-le pour moi. MODIFIER (CODE AJOUTÉ):

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

Vous pouvez consulter cet exemple


Veuillez inclure un exemple de code et ne vous contentez pas de créer des liens vers des sources externes sans contexte ni information. Consultez le centre d'aide pour plus d'informations.
Joseph Farah

5
Malheureusement, mpld3 n'est plus activement maintenu à partir de juillet 2017
Ben Lindsay

L'échantillon de code échoue avec un TypeError: array([1.]) is not JSON serializable.
P-Gn

@ P-Gn suivez simplement l'astuce ici stackoverflow.com/questions/48015030/mpld3-with-python-error MPLD3 est une solution simple pour cela et une fois que la réponse ci-dessus est suivie, cela fonctionne.
Zalakain

1
@Zalakain Malheureusement, mpl3d semble être abandonné .
P-Gn

5

mplcursors a travaillé pour moi. mplcursors fournit une annotation cliquable pour matplotlib. Il est fortement inspiré de mpldatacursor ( https://github.com/joferkington/mpldatacursor ), avec une API très simplifiée

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

J'utilise moi-même, de loin la solution la plus simple pour quelqu'un pressé. Je viens de tracer 70 étiquettes et matplotlibdonne à chaque ligne 10 la même couleur, une telle douleur. mplcursorsle trie cependant.
ajsp

5

Les autres réponses ne répondaient pas à mon besoin d'afficher correctement les info-bulles dans une version récente de la figure matplotlib en ligne de Jupyter. Celui-ci fonctionne cependant:

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

Menant à quelque chose comme l'image suivante en passant sur un point avec la souris: entrez la description de l'image ici



Je ne pouvais pas faire fonctionner ça dans Jupyter Lab. Cela fonctionne-t-il peut-être dans un notebook Jupyter mais pas dans Jupyter Lab?
MD004

3

Si vous utilisez un notebook jupyter, ma solution est aussi simple que:

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

Vous pouvez obtenir quelque chose comme entrez la description de l'image ici


De loin la meilleure solution, seules quelques lignes de code font exactement ce que l'OP a demandé
Tim Johnsen

0

J'ai créé un système d'annotation multiligne à ajouter à: https://stackoverflow.com/a/47166787/10302020 . pour la version la plus récente: https://github.com/AidenBurgess/MultiAnnotationLineGraph

Modifiez simplement les données dans la section inférieure.

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()
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.