Obtendo coordenadas do ponto de dados mais próximo no gráfico matplotlib

9

Eu estou usando matplotlibcom NavigationToolbar2QT. A barra de ferramentas está mostrando a posição do cursor. Mas eu gostaria que o cursor se encaixe no ponto de dados mais próximo (quando estiver próximo o suficiente) ou simplesmente mostre a coordenada do ponto de dados mais próximo. Isso pode ser arranjado de alguma forma?

Pigmalião
fonte
Verifique o link abaixo e veja se ele resolve o seu problema. O link fornece uma função snaptocursor que se parece com o que você está procurando. matplotlib.org/3.1.1/gallery/misc/cursor_demo_sgskip.html
Anupam Chaplot
@AnupamChaplot "Ele usa Matplotlib para desenhar o cursor e pode ser lento, pois isso requer redesenhar a figura a cada movimento do mouse." Eu tenho cerca de 16 parcelas com 10000 pontos CADA no gráfico, portanto, com o redesenho, isso seria bastante lento.
Pygmalion
Se você não deseja redesenhar nada visualmente (por que pedir isso?), Você pode manipular o que é mostrado na barra de ferramentas, como mostrado em matplotlib.org/3.1.1/gallery/images_contours_and_fields/…
ImportanceOfBeingErnest
@ImportanceOfBeingErnest Não entendo sua sugestão. Mas imagine o seguinte: você tem 16 gráficos de linha e cada um deles tem um pico distinto. Você quer saber as coordenadas exatas do pico de um gráfico sem espreitar os dados. Você nunca pode colocar o cursor exatamente no ponto, então isso é altamente impreciso. Assim, programas como o Origin têm a opção de mostrar as coordenadas exatas do ponto mais próximo da posição atual do cursor.
Pygmalion
11
Sim, é isso que cursor_demo_sgskip faz. Mas se você não quiser desenhar o cursor, poderá usar os cálculos desse exemplo e exibir o número resultante na barra de ferramentas, conforme mostrado em image_zcoord
ImportanceOfBeingErnest

Respostas:

6

Se você estiver trabalhando com grandes conjuntos de pontos, aconselho que você 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)

Eu refatorei a kpie'sresposta aqui um pouco. Depois de ckdtreecriado, você pode identificar os pontos mais próximos instantaneamente e vários tipos de informações sobre eles com um pouco de esforço:

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

Exibição interativa da posição do cursor. Se você deseja que as coordenadas do ponto mais próximo sejam exibidas na barra de ferramentas de navegação:

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. Se você quiser outro tipo de interatividade, poderá 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
fonte
Obviamente, isso funcionará perfeitamente se a escala visual x: y for igual a 1. Alguma idéia sobre essa parte do problema, exceto a redimensionamento de pointscada vez que o gráfico é ampliado?
Pygmalion
A alteração da proporção requer alterar as métricas de como a distância é medida nos ckdtrees. Parece que o uso de métricas personalizadas em ckdtrees não é suportado. Portanto, você deve manter os ckdtree.datapontos realistas com escala = 1. Você pointspode ser redimensionado e não há problema se precisar acessar apenas os índices deles.
mathfux
Obrigado. Você sabe, por acaso, se existe uma maneira de acessar facilmente a taxa de escala de reuw para eixos em matplotlib? O que eu encontrei na web foi extremamente complicado.
Pigmalião
IMHO, a melhor solução para o meu problema seria incluir isso como uma opção na matplotlibbiblioteca. Afinal, a biblioteca resgatou as posições dos pontos em algum lugar - afinal, está desenhando-as na trama!
Pigmalião
Você pode tentar set_aspect: matplotlib.org/3.1.3/api/_as_gen/…
mathfux
0

O código a seguir imprimirá as coordenadas do ponto mais próximo ao mouse quando você clicar.

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
fonte
Provavelmente seria capaz de adaptar o código à minha situação (16 parcelas com 10000 pontos cada), mas a ideia era que as coordenadas do ponto fossem impressas na barra de ferramentas de navegação, por exemplo. Isso é possível?
Pygmalion
0

Você poderia subclasse NavigationToolbar2QT e substituir o mouse_movemanipulador. Os atributos xdatae ydatacontêm a posição atual do mouse nas coordenadas da plotagem. Você pode encaixá-lo no ponto de dados mais próximo antes de passar o evento para o mouse_movemanipulador da classe base .

Exemplo completo, com destaque ao ponto mais próximo da trama como um bônus:

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
fonte