Obtenir les coordonnées du point de données le plus proche sur le tracé matplotlib

9

J'utilise matplotlibavec NavigationToolbar2QT. La barre d'outils affiche la position du curseur. Mais j'aimerais que le curseur s'accroche au point de données le plus proche (lorsqu'il est suffisamment proche) ou affiche simplement les coordonnées du point de données le plus proche. Cela peut-il être arrangé d'une manière ou d'une autre?

Pygmalion
la source
Veuillez vérifier le lien ci-dessous et voir s'il résout votre problème. Le lien fournit une fonction snaptocursor qui ressemble à ce que vous recherchez. matplotlib.org/3.1.1/gallery/misc/cursor_demo_sgskip.html
Anupam Chaplot
@AnupamChaplot "Il utilise Matplotlib pour dessiner le curseur et peut être lent car cela nécessite de redessiner la figure à chaque déplacement de la souris." J'ai environ 16 tracés avec 10000 points CHACUN sur le graphique, donc avec un nouveau dessin, ce serait plutôt lent.
Pygmalion le
Si vous ne voulez rien redessiner visuellement (pourquoi le demander alors?), Vous pouvez manipuler ce qui est affiché dans la barre d'outils comme indiqué dans matplotlib.org/3.1.1/gallery/images_contours_and_fields/…
ImportanceOfBeingErnest
@ImportanceOfBeingErnest Je ne comprends pas votre suggestion. Mais imaginez ceci: vous avez 16 tracés linéaires et chacun d'eux a un pic distinct. Vous voulez connaître les coordonnées exactes du pic d'un tracé sans jeter un œil aux données. Vous ne pouvez jamais placer le curseur exactement sur le point, c'est donc très imprécis. Ainsi, des programmes comme Origin ont une option pour afficher les coordonnées exactes du point le plus proche de la position actuelle du curseur.
Pygmalion
1
Oui, c'est ce que fait cursor_demo_sgskip . Mais si vous ne voulez pas dessiner le curseur, vous pouvez utiliser les calculs de cet exemple et afficher à la place le nombre résultant dans la barre d'outils, comme indiqué dans image_zcoord
ImportanceOfBeingErnest

Réponses:

6

Si vous travaillez avec de grands ensembles de points, je vous conseille d'utiliser 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)

J'ai refactorisé la kpie'sréponse ici un peu. Une fois ckdtreecréé, vous pouvez identifier instantanément les points les plus proches et divers types d'informations à leur sujet:

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

Affichage interactif de la position du curseur. Si vous souhaitez que les coordonnées du point le plus proche s'affichent dans la barre d'outils de navigation:

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

Utiliser des événements. Si vous souhaitez un autre type d'interactivité, vous pouvez utiliser des événements:

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
la source
Cela ne fonctionnera bien sûr parfaitement que si l'échelle visuelle x: y est égale à 1. Une idée de cette partie du problème, à l'exception de la mise à pointsl' échelle à chaque fois que le tracé est zoomé?
Pygmalion le
La modification du rapport hauteur / largeur nécessite de modifier les paramètres de mesure de la distance dans ckdtrees. Il semble que l'utilisation de mesures personnalisées sur ckdtrees ne soit pas prise en charge. Par conséquent, vous devez conserver ckdtree.datades points réalistes avec échelle = 1. Vous pointspouvez être redimensionné et il n'y a aucun problème si vous devez accéder uniquement à leurs indices.
mathfux
Merci. Savez-vous, par hasard, s'il existe un moyen d'accéder facilement au rapport d'échelle reuw pour les axes en matplotlib? Ce que j'ai trouvé sur le Web était extrêmement compliqué.
Pygmalion
À mon humble avis, la meilleure solution à mon problème serait de l'inclure comme option dans la matplotlibbibliothèque. Après tout, la bibliothèque a rappelé les positions des points quelque part - après tout, elle les dessine dans l'intrigue!
Pygmalion
Vous pourriez essayer set_aspect: matplotlib.org/3.1.3/api/_as_gen/…
mathfux
0

Le code suivant imprime les coordonnées du point le plus proche de la souris lorsque vous cliquez.

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
la source
Je serais probablement en mesure d'adapter le code à ma situation (16 tracés de 10000 points chacun), mais l'idée était que les coordonnées du point soient imprimées, disons, sur la barre d'outils de navigation. Est-ce possible?
Pygmalion le
0

Vous pouvez sous NavigationToolbar2QT-classer et remplacer le mouse_movegestionnaire. Les attributs xdataet ydatacontiennent la position actuelle de la souris en coordonnées de tracé. Vous pouvez accrocher cela au point de données le plus proche avant de transmettre l'événement au mouse_movegestionnaire de classe de base .

Exemple complet, avec mise en évidence du point le plus proche dans l'intrigue en bonus:

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
la source