Отримання координат найближчої точки даних на графіку matplotlib


9

Я використовую matplotlibс NavigationToolbar2QT. На панелі інструментів відображається положення курсору. Але я хотів би, щоб курсор перекочувався до найближчої точки даних (коли достатньо близько) або просто показував координату найближчої точки даних. Чи можна це якось влаштувати?


Перевірте наведене нижче посилання та побачите, чи вирішує воно вашу проблему. Посилання надає функцію Snaptocursor, яка схожа на те, що ви шукаєте. matplotlib.org/3.1.1/gallery/misc/cursor_demo_sgskip.html
Анупам Чаплот

@AnupamChaplot "Він використовує Matplotlib для малювання курсору і може бути повільним, оскільки для цього потрібно перемальовувати фігуру з кожним рухом миші." У мене на графіку близько 16 сюжетів із 10000 балами ВСЕ, тому з перемальовуванням це буде досить повільним.
Пігмаліон

Якщо ви не хочете нічого перемальовувати візуально (навіщо просити це тоді?), Ви можете маніпулювати тим, що відображається на панелі інструментів, як показано на matplotlib.org/3.1.1/gallery/images_contours_and_fields/…
ВажливістьOfBeingErnest

@ImportanceOfBeingErnest Я не розумію вашої пропозиції. Але уявіть це: у вас 16 лінійних ділянок, і кожен з них має чіткий пік. Ви хочете знати точні координати піку однієї ділянки, не заглядаючи в дані. Ніколи не можна ставити курсор точно на точку, тому це вкрай неточно. Таким програмам, як Origin, є можливість відобразити точні координати найближчої точки до поточної позиції курсору.
Пігмаліон

1
Так, це робить cursor_demo_sgskip . Але якщо ви не хочете малювати курсор, ви можете використовувати обчислення з цього прикладу і замість цього відобразити отримане число на панелі інструментів, як показано на image_zcoord
ImportanceOfBeingErnest

Відповіді:


6

Якщо ви працюєте з великими наборами точок, раджу використовувати 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'sтрохи відремонтував відповідь. Після ckdtreeстворення ви зможете миттєво ідентифікувати найближчі точки та різного роду інформацію про них:

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

Інтерактивне відображення положення курсору. Якщо ви хочете, щоб координати найближчої точки відображалися на панелі інструментів навігації:

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

Використання подій. Якщо ви хочете іншого типу інтерактивності, ви можете використовувати події:

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

Це, звичайно, спрацює бездоганно, лише якщо візуальна шкала x: y дорівнює 1. Будь-яке уявлення про цю частину проблеми, за винятком повторного масштабування pointsкожного разу, коли масштаб збільшується?
Пігмаліон

Зміна співвідношення сторін вимагає зміни метрик того, як вимірюється відстань у ckdtrees. Схоже, використання користувацьких показників на ckdtrees не підтримується. Тому ви повинні зберігати ckdtree.dataяк реалістичні бали зі шкалою = 1. Ви pointsможете змінити масштаб, і немає жодної проблеми, якщо вам потрібно отримати доступ лише до їх індексів.
mathfux

Дякую. Чи знаєте ви, чи випадково, якщо є спосіб легко отримати доступ до відношення шкали відновлення для осей matplotlib? Те, що я знайшов в Інтернеті, було надзвичайно складним.
Пігмаліон

ІМХО найкращим рішенням моєї проблеми було б включити це як варіант у matplotlibбібліотеку. Зрештою, бібліотека десь переосмислила точкові позиції - зрештою, це малює їх у сюжеті!
Пігмаліон

Ви можете спробувати set_aspect: matplotlib.org/3.1.3/api/_as_gen/…
mathfux

0

Наступний код друкує координати точки, найближчої до миші при натисканні кнопки.

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

Можливо, я міг би адаптувати код до моєї ситуації (16 сюжетів із 10000 балів у кожному), але ідея полягала в тому, що координати точки надруковані на, скажімо, панелі інструментів навігації. Це можливо?
Пігмаліон

0

Ви можете підкласифікувати NavigationToolbar2QTта змінити mouse_moveобробник. xdataІ ydataатрибути містять поточний стан курсору миші в координатах ділянки. Ви можете прив'язати це до найближчої точки даних, перш ніж передати подію mouse_moveобробнику базового класу .

Повний приклад із виділенням найближчої точки сюжету як бонусу:

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_()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.