Uzyskiwanie współrzędnych najbliższego punktu danych na wykresie matplotlib

9

Używam matplotlibz NavigationToolbar2QT. Pasek narzędzi pokazuje pozycję kursora. Chciałbym jednak, aby kursor przyciągał do najbliższego punktu danych (gdy jest wystarczająco blisko) lub po prostu pokazywał współrzędne najbliższego punktu danych. Czy można to jakoś zaaranżować?

Pigmalion
źródło
Sprawdź poniższy link i sprawdź, czy to rozwiąże problem. Łącze udostępnia funkcję snaptocursor, która wygląda podobnie do tego, czego szukasz. matplotlib.org/3.1.1/gallery/misc/cursor_demo_sgskip.html
Anupam Chaplot
@AnupamChaplot "Używa Matplotlib do rysowania kursora i może być powolny, ponieważ wymaga przerysowania figury przy każdym ruchu myszy." Mam około 16 wykresów z KAŻDYM 10000 punktami na wykresie, więc przy przerysowaniu byłoby to raczej powolne.
Pygmalion
Jeśli nie chcesz przerysować niczego wizualnie (po co w takim razie pytać?), Możesz manipulować tym, co jest wyświetlane na pasku narzędzi, jak pokazano na matplotlib.org/3.1.1/gallery/images_contours_and_fields/…
ImportanceOfBeingErnest
@ImportanceOfBeingErnest Nie rozumiem twojej sugestii. Ale wyobraź sobie: masz 16 wykresów liniowych, a każdy z nich ma odrębny pik. Chcesz znać dokładne współrzędne szczytu jednego wykresu bez zaglądania do danych. Nigdy nie możesz ustawić kursora dokładnie na punkcie, więc jest to bardzo nieprecyzyjne. Programy takie jak Origin mają opcję pokazania dokładnych współrzędnych najbliższego punktu do bieżącej pozycji kursora.
Pygmalion
1
Tak, właśnie to robi kursor_demo_sgskip . Ale jeśli nie chcesz, aby narysować kursorem, można skorzystać z obliczeń z tego przykładu i zamiast wyświetlać wynikowy numer w pasku narzędzi, jak pokazano na image_zcoord
ImportanceOfBeingErnest

Odpowiedzi:

6

Jeśli pracujesz z dużymi zestawami punktów, radzę użyć CKDtrees:

import matplotlib.pyplot as plt
import numpy as np
import scipy.spatial

points = np.column_stack([np.random.rand(50), np.random.rand(50)])
fig, ax = plt.subplots()
coll = ax.scatter(points[:,0], points[:,1])
ckdtree = scipy.spatial.cKDTree(points)

kpie'sTutaj trochę zmieniłem odpowiedź. Po ckdtreeutworzeniu, możesz z niewielkim wysiłkiem zidentyfikować najbliższe punkty i różnego rodzaju informacje na ich temat:

def closest_point_distance(ckdtree, x, y):
    #returns distance to closest point
    return ckdtree.query([x, y])[0]

def closest_point_id(ckdtree, x, y):
    #returns index of closest point
    return ckdtree.query([x, y])[1]

def closest_point_coords(ckdtree, x, y):
    # returns coordinates of closest point
    return ckdtree.data[closest_point_id(ckdtree, x, y)]
    # ckdtree.data is the same as points

Interaktywne wyświetlanie pozycji kursora. Jeśli chcesz, aby współrzędne najbliższego punktu były wyświetlane na pasku narzędzi nawigacji:

def val_shower(ckdtree):
    #formatter of coordinates displayed on Navigation Bar
    return lambda x, y: '[x = {}, y = {}]'.format(*closest_point_coords(ckdtree, x, y))

plt.gca().format_coord = val_shower(ckdtree)
plt.show()

Korzystanie z wydarzeń. Jeśli chcesz innego rodzaju interaktywności, możesz użyć zdarzeń:

def onclick(event):
    if event.inaxes is not None:
        print(closest_point_coords(ckdtree, event.xdata, event.ydata))

fig.canvas.mpl_connect('motion_notify_event', onclick)
plt.show()
mathfux
źródło
Będzie to oczywiście działało bezbłędnie tylko wtedy, gdy skala wizualna x: y jest równa 1. Masz pojęcie o tej części problemu, z wyjątkiem ponownego skalowania przy pointskażdym powiększeniu wykresu?
Pygmalion
Zmiana współczynnika kształtu wymaga zmiany wskaźników pomiaru odległości w ckdtrees. Wygląda na to, że używanie niestandardowych metryk w ckdtrees nie jest obsługiwane. Dlatego musisz zachować ckdtree.datarealistyczne punkty ze skalą = 1. pointsMożesz przeskalować i nie ma problemu, jeśli potrzebujesz dostępu tylko do ich indeksów.
mathfux
Dzięki. Czy wiesz przypadkiem, czy istnieje sposób na łatwy dostęp do współczynnika skali reuw dla osi w matplotlib? To, co znalazłem w sieci, było niezwykle skomplikowane.
Pygmalion
IMHO najlepszym rozwiązaniem mojego problemu byłoby włączenie tego jako opcji do matplotlibbiblioteki. W końcu biblioteka gdzieś odwołała pozycje punktowe - w końcu rysuje je na wykresie!
Pygmalion
Możesz spróbować set_aspect: matplotlib.org/3.1.3/api/_as_gen/…
mathfux
0

Poniższy kod wyświetli współrzędne punktu najbliższego myszy po kliknięciu.

import matplotlib.pyplot as plt
import numpy as np
np.random.seed(19680801)
N = 50
x = np.random.rand(N)
y = np.random.rand(N)
fig,ax = plt.subplots()
plt.scatter(x, y)
points = list(zip(x,y))
def distance(a,b):
    return(sum([(k[0]-k[1])**2 for k in zip(a,b)])**0.5)
def onclick(event):
    dists = [distance([event.xdata, event.ydata],k) for k in points]
    print(points[dists.index(min(dists))])
fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()
kpie
źródło
Prawdopodobnie byłbym w stanie dostosować kod do mojej sytuacji (16 wykresów po 10000 punktów każdy), ale pomysł polegał na tym, że współrzędne kropki są drukowane, powiedzmy, na pasku narzędzi nawigacyjnych. Czy to jest możliwe?
Pygmalion
0

Możesz podklasować NavigationToolbar2QTi zastąpić mouse_movemoduł obsługi. xdataI ydataatrybuty zawierają aktualną pozycję myszy we współrzędnych fabuły. Możesz przyciągnąć to do najbliższego punktu danych przed przekazaniem zdarzenia do modułu mouse_moveobsługi klasy podstawowej .

Pełny przykład z wyróżnieniem najbliższego punktu na wykresie:

import sys

import numpy as np

from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas, NavigationToolbar2QT
from matplotlib.figure import Figure


class Snapper:
    """Snaps to data points"""

    def __init__(self, data, callback):
        self.data = data
        self.callback = callback

    def snap(self, x, y):
        pos = np.array([x, y])
        distances = np.linalg.norm(self.data - pos, axis=1)
        dataidx = np.argmin(distances)
        datapos = self.data[dataidx,:]
        self.callback(datapos[0], datapos[1])
        return datapos


class SnappingNavigationToolbar(NavigationToolbar2QT):
    """Navigation toolbar with data snapping"""

    def __init__(self, canvas, parent, coordinates=True):
        super().__init__(canvas, parent, coordinates)
        self.snapper = None

    def set_snapper(self, snapper):
        self.snapper = snapper

    def mouse_move(self, event):
        if self.snapper and event.xdata and event.ydata:
            event.xdata, event.ydata = self.snapper.snap(event.xdata, event.ydata)
        super().mouse_move(event)


class Highlighter:
    def __init__(self, ax):
        self.ax = ax
        self.marker = None
        self.markerpos = None

    def draw(self, x, y):
        """draws a marker at plot position (x,y)"""
        if (x, y) != self.markerpos:
            if self.marker:
                self.marker.remove()
                del self.marker
            self.marker = self.ax.scatter(x, y, color='yellow')
            self.markerpos = (x, y)
            self.ax.figure.canvas.draw()


class ApplicationWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self._main = QtWidgets.QWidget()
        self.setCentralWidget(self._main)
        layout = QtWidgets.QVBoxLayout(self._main)
        canvas = FigureCanvas(Figure(figsize=(5,3)))
        layout.addWidget(canvas)
        toolbar = SnappingNavigationToolbar(canvas, self)
        self.addToolBar(toolbar)

        data = np.random.randn(100, 2)
        ax = canvas.figure.subplots()
        ax.scatter(data[:,0], data[:,1])

        self.highlighter = Highlighter(ax)
        snapper = Snapper(data, self.highlighter.draw)
        toolbar.set_snapper(snapper)


if __name__ == "__main__":
    qapp = QtWidgets.QApplication(sys.argv)
    app = ApplicationWindow()
    app.show()
    qapp.exec_()
Alexander Rossmanith
źródło