Obteniendo coordenadas del punto de datos más cercano en el diagrama matplotlib

9

Estoy usando matplotlibcon NavigationToolbar2QT. La barra de herramientas muestra la posición del cursor. Pero me gustaría que el cursor se ajuste al punto de datos más cercano (cuando esté lo suficientemente cerca) o simplemente muestre la coordenada del punto de datos más cercano. ¿Se puede arreglar de alguna manera?

Pigmalión
fuente
Verifique el siguiente enlace y vea si resuelve su problema. El enlace proporciona una función snaptocursor que se parece a lo que está buscando. matplotlib.org/3.1.1/gallery/misc/cursor_demo_sgskip.html
Anupam Chaplot
@AnupamChaplot "Utiliza Matplotlib para dibujar el cursor y puede ser lento ya que esto requiere volver a dibujar la figura con cada movimiento del mouse". Tengo alrededor de 16 parcelas con 10000 puntos CADA UNA en el gráfico, por lo que con el rediseño esto sería bastante lento.
Pigmalión
Si no desea volver a dibujar nada visualmente (¿por qué pedir eso entonces?), Puede manipular lo que se muestra en la barra de herramientas como se muestra en matplotlib.org/3.1.1/gallery/images_contours_and_fields/…
ImportanceOfBeingErnest
@ImportanceOfBeingErnest No entiendo tu sugerencia. Pero imagine esto: tiene 16 gráficos de líneas y cada uno de ellos tiene un pico distinto. Desea conocer las coordenadas exactas del pico de una parcela sin echar un vistazo a los datos. Nunca puede colocar el cursor exactamente en el punto, por lo que esto es muy impreciso. Entonces, los programas como Origin tienen una opción para mostrar las coordenadas exactas del punto más cercano a la posición actual del cursor.
Pigmalión
1
Sí, eso es lo que hace cursor_demo_sgskip . Pero si no desea dibujar el cursor, puede usar los cálculos de ese ejemplo y, en su lugar, mostrar el número resultante en la barra de herramientas, como se muestra en image_zcoord
ImportanceOfBeingErnest

Respuestas:

6

Si está trabajando con grandes conjuntos de puntos, le aconsejo que use 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)

Refactoré la kpie'srespuesta aquí un poco. Una vez ckdtreecreado, puede identificar los puntos más cercanos al instante y diversos tipos de información sobre ellos con un poco de esfuerzo:

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

Visualización interactiva de la posición del cursor. Si desea que se muestren las coordenadas del punto más cercano en la barra de herramientas de navegación:

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

Usando eventos. Si desea otro tipo de interactividad, puede usar eventos:

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
fuente
Por supuesto, esto funcionará sin problemas solo si la escala visual x: y es igual a 1. ¿Alguna idea sobre esta parte del problema, excepto para volver a escalar pointscada vez que se amplía la gráfica?
Pigmalión
Cambiar la relación de aspecto requiere cambiar las métricas de cómo se mide la distancia en ckdtrees. Parece que el uso de métricas personalizadas en ckdtrees no es compatible. Por lo tanto, debe mantener ckdtree.datacomo puntos realistas con scale = 1. Su pointspuede ser reescalado y no hay problema si solo necesita acceder a sus índices.
Mathfux
Gracias. ¿Sabes, por casualidad, si hay una manera de acceder fácilmente a la relación de escala de reajuste para los ejes matplotlib? Lo que encontré en la web fue extremadamente complicado.
Pigmalión
En mi humilde opinión, la mejor solución para mi problema sería incluir eso como una opción en la matplotlibbiblioteca. Después de todo, la biblioteca ha rediseñado posiciones de puntos en algún lugar; después de todo, ¡las está dibujando en la trama!
Pigmalión
Puede probar set_aspect: matplotlib.org/3.1.3/api/_as_gen/…
mathfux
0

El siguiente código imprimirá las coordenadas del punto más cercano al mouse cuando haga clic.

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
fuente
Probablemente podría adaptar el código a mi situación (16 parcelas con 10000 puntos cada una), pero la idea era que las coordenadas del punto se imprimen, por ejemplo, en la barra de herramientas de navegación. ¿Es eso posible?
Pigmalión
0

Podría subclasificar NavigationToolbar2QTy anular el mouse_movecontrolador. Los atributos xdatay ydatacontienen la posición actual del mouse en las coordenadas de la trama. Puede ajustarlo al punto de datos más cercano antes de pasar el evento a la clase basemouse_move controlador de la .

Ejemplo completo, con el resaltado del punto más cercano en la trama como un bono:

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
fuente