Czy można wyświetlać etykiety po najechaniu kursorem na punkt w matplotlib?

147

Do tworzenia wykresów punktowych używam matplotlib. Każdy punkt na wykresie punktowym jest powiązany z nazwanym obiektem. Chciałbym móc zobaczyć nazwę obiektu po najechaniu kursorem na punkt na wykresie punktowym skojarzonym z tym obiektem. W szczególności dobrze byłoby móc szybko zobaczyć nazwy punktów, które są wartościami odstającymi. Najbliższą rzeczą, jaką udało mi się znaleźć podczas wyszukiwania tutaj, jest polecenie adnotacji, ale wydaje się, że tworzy ono stałą etykietę na wykresie. Niestety, biorąc pod uwagę liczbę punktów, które mam, wykres punktowy byłby nieczytelny, gdybym oznaczył każdy punkt etykietą. Czy ktoś zna sposób na tworzenie etykiet, które pojawiają się tylko wtedy, gdy kursor znajdzie się w pobliżu tego punktu?

jdmcbr
źródło
2
Osoby kończące tutaj wyszukiwanie mogą również chcieć sprawdzić tę odpowiedź , która jest dość złożona, ale może być odpowiednia w zależności od wymagań.
ImportanceOfBeingErnest

Odpowiedzi:

133

Wydaje się, że żadna z pozostałych odpowiedzi nie odpowiada na to pytanie. Oto kod, który używa rozproszenia i pokazuje adnotację po najechaniu kursorem na punkty rozrzutu.

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

wprowadź opis obrazu tutaj

Ponieważ ludzie chcą również używać tego rozwiązania dla linii plotzamiast rozproszenia, poniższe rozwiązanie byłoby tym samym rozwiązaniem plot(które działa nieco inaczej).

Jeśli ktoś szuka rozwiązania dla linii na podwójnych osiach, zapoznaj się z artykułem Jak wyświetlić etykiety po najechaniu kursorem na punkt na wielu osiach?

W przypadku, gdy ktoś szuka rozwiązania dla wykresów słupkowych, zapoznaj się np. Z tą odpowiedzią .

ImportanceOfBeingErnest
źródło
1
Bardzo dobrze! Jedna uwaga, zauważyłem, że ind["ind"]jest to lista indeksów dla wszystkich punktów pod kursorem. Oznacza to, że powyższy kod faktycznie daje dostęp do wszystkich punktów na danej pozycji, a nie tylko do najwyższego punktu. Na przykład, jeśli masz dwa nakładające się punkty, tekst może być odczytany 1 2, B Club nawet 1 2 3, B C Djeśli masz 3 nakładające się punkty.
Jvinniec
@Jvinniec Dokładnie, celowo jest jeden taki przypadek na powyższym wykresie (zielona i czerwona kropka w punkcie x ~ 0,4). Jeśli najedziesz na niego kursorem, wyświetli się 0 8, A I(patrz zdjęcie ).
ImportanceOfBeingErnest
@ImportanceOfBeingErnest to świetny kod, ale podczas najeżdżania i poruszania się po punkcie wywołuje fig.canvas.draw_idle()wiele razy (nawet zmienia kursor na bezczynny). Rozwiązałem to zapisując poprzedni indeks i sprawdzając, czy ind["ind"][0] == prev_ind. Następnie aktualizuj tylko wtedy, gdy przechodzisz z jednego punktu do drugiego (aktualizuj tekst), przestań najeżdżać (uczyń adnotację niewidoczną) lub zacznij najeżdżać (uczyń adnotację widoczną). Dzięki tej zmianie jest o wiele bardziej czysty i wydajny.
Sembei Norimaki
3
@Konstantin Tak, to rozwiązanie będzie działać %matplotlib notebookna notebooku IPython / Jupyter.
ImportanceOfBeingErnest
1
@OriolAbril (i wszyscy inni), Jeśli masz problem, który powstał podczas modyfikacji kodu z tej odpowiedzi, zadaj mu pytanie, link do tej odpowiedzi i pokaż kod, który próbowałeś. Nie mam sposobu, aby dowiedzieć się, co jest nie tak z każdym z twoich kodów, nie widząc go.
ImportanceOfBeingErnest
66

To rozwiązanie działa, gdy najeżdżasz na linię bez konieczności jej klikania:

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
źródło
1
Bardzo przydatne + 1ed. Prawdopodobnie będziesz musiał to „odbić”, ponieważ motion_notify_event będzie się powtarzać dla ruchu wewnątrz obszaru krzywej. Wydaje się, że proste sprawdzenie, czy obiekt krzywej jest równy poprzedniej krzywej, działa.
wiał
5
Hmm - to nie zadziałało dla mnie od razu po wyjęciu z pudełka (tak niewiele rzeczy robi z matplotlib...) - czy to działa z ipython/ jupyternotebookami? Czy działa również, gdy istnieje wiele wątków pobocznych? A co z wykresem słupkowym, a nie liniowym?
dwanderson
12
Spowoduje to wydrukowanie etykiety na konsoli podczas zawisu. A co z wyświetlaniem etykiety na zdjęciu po najechaniu kursorem? Zrozumiałem, że to jest pytanie.
Nikana Reklawyks
@mbernasocchi wielkie dzięki, co muszę podać w argumencie gid, jeśli chcę zobaczyć histogram (inny dla każdego punktu w rozproszeniu) lub, jeszcze lepiej, mapę cieplną histogramu 2D?
Amitai
@NikanaReklawyks Dodałem odpowiedź, która faktycznie odpowiada na pytanie.
ImportanceOfBeingErnest
37

Z 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
źródło
Robi to, czego potrzebuję, dziękuję! Jako bonus, aby go zaimplementować, przepisałem swój program tak, że zamiast tworzyć dwa oddzielne wykresy punktowe w różnych kolorach na tej samej figurze, aby przedstawić dwa zestawy danych, skopiowałem metodę przypisywania koloru do punktu z przykładu. To sprawiło, że mój program był nieco prostszy do odczytania i mniej kodu. A teraz znajdź przewodnik dotyczący konwersji koloru na liczbę!
jdmcbr
1
Dotyczy to wykresów punktowych. A co z wykresami liniowymi? Próbowałem, żeby to na nich zadziałało, ale nie działa. Czy istnieje obejście?
Sohaib
@Sohaib Zobacz moją odpowiedź
texasflood
Mam pytanie w tej sprawie. Kiedy wykreślam punkty w ten sposób: plt.scatter (X_reduced [y == i, 0], X_reduced [y == i, 1], c = c, label = target_name, picker = True) z suwakiem dla i, c i target_name, czy więc kolejność moich indeksów jest pomieszana? I nie mogę już sprawdzić, do którego punktu danych należy?
Chris,
Wydaje się, że to nie działa w przypadku notebooków jupyter 5 z ipythonem 5. Czy istnieje łatwy sposób, aby to naprawić? printOświadczenie powinno również użyć nawiasów dla kompatybilności z Pythona 3
nealmcb
14

Niewielka zmiana na przykładzie podanym w 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()

To kreśli prostoliniową fabułę, o co prosił Sohaib

texasflood
źródło
5

mpld3 rozwiąż to za mnie. EDYCJA (DODANO KOD):

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

Możesz sprawdzić ten przykład

juliański
źródło
Dołącz przykładowy kod, a nie tylko odsyłaj do zewnętrznych źródeł bez kontekstu lub informacji. Więcej informacji znajdziesz w Centrum pomocy .
Joseph Farah
5
niestety mpld3 nie jest już aktywnie utrzymywany od lipca 2017 r.
Ben Lindsay
Przykładowy kod kończy się niepowodzeniem z plikiem TypeError: array([1.]) is not JSON serializable.
P-Gn
@ P-Gn po prostu podążaj za sztuczką tutaj stackoverflow.com/questions/48015030/mpld3-with-python-error MPLD3 jest prostym rozwiązaniem i po wykonaniu powyższej odpowiedzi działa.
Zalakain
1
@Zalakain Niestety, wydaje się, że mpl3d został porzucony .
P-Gn
5

Pracowały dla mnie mplcursors. mplcursors zapewnia klikalne adnotacje dla matplotlib. Jest mocno zainspirowany mpldatacursor ( https://github.com/joferkington/mpldatacursor ), ze znacznie uproszczonym interfejsem API

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
źródło
Sam tego używam, zdecydowanie najłatwiejsze rozwiązanie dla kogoś, kto się spieszy. Właśnie wykreśliłem 70 etykiet i sprawiam, matplotlibże każda dziesiąta linia ma ten sam kolor, taki ból. mplcursorsrozwiązuje to jednak.
ajsp
5

Inne odpowiedzi nie dotyczyły mojej potrzeby prawidłowego wyświetlania podpowiedzi w najnowszej wersji wbudowanej figury matplotlib Jupyter. Ten jednak działa:

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

Prowadzące do czegoś podobnego do poniższego obrazu podczas przechodzenia przez punkt za pomocą myszy: wprowadź opis obrazu tutaj

Farzad Vertigo
źródło
3
Źródłem tego ( nieprzypisanego
Victoria Stuart
Nie mogłem tego uruchomić w laboratorium jupyter. Czy może działa w notebooku jupyter, ale nie w laboratorium jupyter?
MD004
3

Jeśli używasz notebooka jupyter, moje rozwiązanie jest tak proste, jak:

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

Możesz dostać coś takiego wprowadź opis obrazu tutaj

Yuchao Jiang
źródło
Zdecydowanie najlepsze rozwiązanie, tylko kilka wierszy kodu robi dokładnie to, o co prosił OP
Tim Johnsen
0

Zrobiłem wielowierszowy system adnotacji do dodania: https://stackoverflow.com/a/47166787/10302020 . najbardziej aktualna wersja: https://github.com/AidenBurgess/MultiAnnotationLineGraph

Po prostu zmień dane w dolnej sekcji.

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
źródło