Getting coordinates of the closest data point on matplotlib plot - python

I am using matplotlib with NavigationToolbar2QT. The toolbar is showing the position of the cursor. But I would like that the cursor snaps to the nearest data point (when close enough) or simply show the coordinate of nearest data point. Can that be somehow arranged?

If you are working with large sets of points, I advice you to 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)
I refactored kpie's answer here little bit. Once ckdtree is created, you can identify closest points instantly and various kind of information about them with a little effort:
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
Interactive display of cursor position.
If you want coordinates of the closest point to be displayed on Navigation Toolbar:
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()
Using events.
If you want another kind of interactivity, you can use events:
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()

You could subclass NavigationToolbar2QT and override the mouse_move handler. The xdata and ydata attributes contain the current mouse position in plot coordinates. You can snap that to the closest data point before passing the event to the base class mouse_move handler.
Full example, with highlighting of the closest point in the plot as a 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_()

The following code will print the coordinates of the dot closest to the mouse when you click.
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()

Another possibility is to use the picking support axes already have. See this section in the event handling docs.
Jim

Related

How do you increase the pickradius of a matplotlib Line2D artist?

I would like to create plot where each point is an individual artist that can be picked. This is my current solution:
import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import \
FigureCanvasQTAgg as FigureCanvas
import matplotlib.patheffects as PathEffects
from PyQt5.QtWidgets import QDialog, QApplication, QVBoxLayout
class MainWindow(QDialog):
def __init__(self):
super().__init__()
self.fig, self.ax = plt.subplots()
self.canvas = FigureCanvas(self.fig)
a = [np.random.randint(100) for _ in range(100)]
b = [np.random.randint(100) for _ in range(100)]
self.artists = []
self.last_artist = None
for x, y in zip(a, b):
artist = self.ax.plot(
x, y, 'o', picker=True, pickradius=6, color='#ff4500'
)
self.artists += artist
self.canvas.draw()
self.cid_motion = self.fig.canvas.mpl_connect(
'motion_notify_event', self.hover
)
layout = QVBoxLayout()
layout.addWidget(self.canvas)
self.setLayout(layout)
def hover(self, event):
if event.inaxes == self.ax:
ind = 0
cont = None
while (
ind in range(len(self.artists))
and not cont
):
artist = self.artists[ind]
cont, _ = artist.contains(event)
if cont:
if artist is not self.last_artist:
if self.last_artist is not None:
self.last_artist.set_path_effects(
[PathEffects.Normal()]
)
self.last_artist.set_zorder(2)
artist.set_path_effects(
[PathEffects.withStroke(
linewidth=7, foreground="c", alpha=0.4
)]
)
artist.set_zorder(3)
self.last_artist = artist
ind += 1
if not cont and self.last_artist is not None:
self.last_artist.set_path_effects([PathEffects.Normal()])
self.last_artist.set_zorder(2)
self.last_artist = None
self.canvas.draw()
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
However, the highlighting only works if you hover exactly over the center of a data point, it doesn't matter if you increase the pickradius. So I thought maybe I could change the contains method, but I don't know how. I found that matplotlib Artists come with a set_contains method which you can use to come up with your own contains method. But I don't know how to do that. I was hoping I could learn from how the default contains method is implemented and looked at the source code, but unfortunately this doesn't expain anything.
pickradius combined with picker as bool should achieve what you intend to. However, it is not doing so. On the other hand, picker if defined a float, is used as the pick radius in points. Therefore, specifying the tolerance directly to picker makes thing work. Change the following line:
artist = self.ax.plot(
x, y, 'o', picker=True, pickradius=6, color='#ff4500'
)
to
artist = self.ax.plot(
x, y, 'o', picker=6, color='#ff4500'
)

Plot fast changing data using OOP approach

I'm trying to plot the results from each iteration of an algorithm (data points are obtained quite fast). I´ve already looked at matplotlib.animation and a bunch of solutions using other packages but none of them uses an OOP approach.
I came up with this code:
import matplotlib.animation as animation
import random
class LivePlot:
def __init__(self, title, x_label, y_label):
self.x = []
self.y = []
self.fig = plt.figure()
self.ax = self.fig.add_subplot(1, 1, 1)
# Create a blank line (will be updated in animate)
self.line, = self.ax.plot([], [])
# Add labels
plt.xlabel(x_label)
plt.ylabel(y_label)
plt.title(title)
self.anim = animation.FuncAnimation(self.fig, self.animate,
init_func=self.__init,
blit=True)
def __init(self):
self.line.set_data([], [])
return self.line,
def animate(self, i):
# Update line with new Y values
self.line.set_data(self.x, self.y)
return self.line,
# Set up plot to call animate() function periodically
def plot(self, xs, ys):
self.x.append(xs)
self.y.append(ys)
plt.show()
if __name__ == '__main__':
pl = LivePlot(title='test', x_label='iter', y_label='score')
for x in range(100):
y = random.randint(1,4)
pl.plot(x, y)
It doesn´t work, the plot window disappear almost immediately without plotting any data.

PyQt / Matplotlib: How to redraw / update a 3D surface plot in a window?

While executing the PyQt program the updated figure is plotting over the first plot.I want the plot to be appeared in a Window which is shown in a QWidget.The following idea works for the 3D line as well as point plots but not for the surface plots.
# imports
import sys
from PyQt4.QtCore import * # Python Qt4 bindings for GUI objects
from PyQt4.QtGui import *
from math import * # For fun log()
import numpy as np
from matplotlib.figure import Figure # For Matplotlib Figure Object
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt4agg \
import FigureCanvasQTAgg as FigureCanvas
from mpl_toolkits.mplot3d import Axes3D
class ThreeDSurface_GraphWindow(FigureCanvas): #Class for 3D window
def __init__(self):
self.fig =plt.figure(figsize=(7,7))
FigureCanvas.__init__(self, self.fig) #creating FigureCanvas
self.axes = self.fig.gca(projection='3d')#generates 3D Axes object
self.axes.hold(False)#clear axes on each run
self.setWindowTitle("Main") # sets Window title
def DrawGraph(self, x, y, z):#Fun for Graph plotting
self.axes.plot_surface(x, y, z) #plots the 3D surface plot
self.draw()
class TabConfigWidget(QWidget):# The QWidget in which the 3D window is been embedded
def __init__(self, parent=None):
super(TabConfigWidget, self).__init__(parent)
self.Combo=QComboBox()#ComboBox for change index
self.Combo.addItems(['Graph1','Graph2'])#add indexes to ComboBox
self.Combo.currentIndexChanged.connect(self.IndexChanged)#Invokes Fun IndexChanged when current index changes
self.ThreeDWin = ThreeDSurface_GraphWindow()#creating 3D Window
MainLayout = QGridLayout()# Layout for Main Tab Widget
MainLayout.setRowMinimumHeight(0, 5) #setting layout parameters
MainLayout.setRowMinimumHeight(2, 10)
MainLayout.setRowMinimumHeight(4, 5)
MainLayout.addWidget(self.Combo, 1, 1)#add GroupBox to Main layout
MainLayout.addWidget(self.ThreeDWin,2,1)#add 3D Window to Main layout
self.setLayout(MainLayout) #sets Main layout
x=np.linspace(-6,6,30) #X coordinates
y=np.linspace(-6,6,30) #Y coordinates
X,Y=np.meshgrid(x,y) #Forming MeshGrid
Z=self.f(X,Y)
self.ThreeDWin.DrawGraph(X,Y,Z)#call Fun for Graph plot
def f(self,x,y):#For Generating Z coordinates
return np.sin(np.sqrt(x**2+y**2))
def IndexChanged(self):#Invoked when the ComboBox index changes
x = np.linspace(-2, 6, 40) #X coordinates
y = np.linspace(-2, 6, 40) #Y coordinates
X, Y = np.meshgrid(x, y) #Forming MeshGrid
Z = self.f(X, Y)
self.ThreeDWin.DrawGraph(X, Y, Z) #call Fun for Graph plot
class GSPP(QTabWidget): #This is the Main QTabWidget
def __init__(self, parent=None):
super(GSPP, self).__init__(parent)
self.tab1 = TabConfigWidget()
self.addTab(self.tab1, "Tab1 ") #adding the QWidget
def Refresh(self): #Fun to refresh
return
if __name__ == '__main__': #The Fun for Main()
app = QApplication(sys.argv)
Window = GSPP()
Window.setWindowTitle("Main")
qr = Window.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
Window.move(qr.topLeft())
Window.showMaximized()
app.processEvents()
sys.exit(app.exec_())
You just need to clear the axes before plotting:
def DrawGraph(self, x, y, z): # Fun for Graph plotting
self.axes.clear() # <<<
self.axes.plot_surface(x, y, z) # plots the 3D surface plot
I also added some random numbers to make the update visible. This is the full code:
import sys
from PyQt4.QtCore import * # Python Qt4 bindings for GUI objects
from PyQt4.QtGui import *
from math import * # For fun log()
import numpy as np
from matplotlib.figure import Figure # For Matplotlib Figure Object
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from mpl_toolkits.mplot3d import Axes3D
class ThreeDSurface_GraphWindow(FigureCanvas): # Class for 3D window
def __init__(self):
self.fig =plt.figure(figsize=(7,7))
FigureCanvas.__init__(self, self.fig) # creating FigureCanvas
self.axes = self.fig.gca(projection='3d') # generates 3D Axes object
self.axes.hold(False)#clear axes on each run
self.setWindowTitle("Main") # sets Window title
def DrawGraph(self, x, y, z): # Fun for Graph plotting
self.axes.clear()
self.axes.plot_surface(x, y, z) # plots the 3D surface plot
self.draw()
class TabConfigWidget(QWidget): # The QWidget in which the 3D window is been embedded
def __init__(self, parent=None):
super(TabConfigWidget, self).__init__(parent)
self.Combo=QComboBox() # ComboBox for change index
self.Combo.addItems(['Graph1','Graph2']) # add indexes to ComboBox
self.Combo.currentIndexChanged.connect(self.IndexChanged) # Invokes Fun IndexChanged when current index changes
self.ThreeDWin = ThreeDSurface_GraphWindow() # creating 3D Window
MainLayout = QGridLayout()# Layout for Main Tab Widget
MainLayout.setRowMinimumHeight(0, 5) # setting layout parameters
MainLayout.setRowMinimumHeight(2, 10)
MainLayout.setRowMinimumHeight(4, 5)
MainLayout.addWidget(self.Combo, 1, 1) # add GroupBox to Main layout
MainLayout.addWidget(self.ThreeDWin,2,1) # add 3D Window to Main layout
self.setLayout(MainLayout) # sets Main layout
x=np.linspace(-6,6,30) # X coordinates
y=np.linspace(-6,6,30) # Y coordinates
X,Y=np.meshgrid(x,y) # Forming MeshGrid
Z=self.f(X,Y)
self.ThreeDWin.DrawGraph(X,Y,Z) # call Fun for Graph plot
def f(self,x,y): # For Generating Z coordinates
return np.sin(np.sqrt(x**2+y**2))
def IndexChanged(self): # Invoked when the ComboBox index changes
x = np.linspace(np.random.randint(-5,0), np.random.randint(5,10), 40) # X coordinates
y = np.linspace(np.random.randint(-5,0), np.random.randint(5,10), 40) # Y coordinates
X, Y = np.meshgrid(x, y) # Forming MeshGrid
Z = self.f(X, Y)
self.ThreeDWin.DrawGraph(X, Y, Z) # call Fun for Graph plot
class GSPP(QTabWidget): # This is the Main QTabWidget
def __init__(self, parent=None):
super(GSPP, self).__init__(parent)
self.tab1 = TabConfigWidget()
self.addTab(self.tab1, "Tab1 ") # adding the QWidget
def Refresh(self): # Fun to refresh
return
if __name__ == '__main__': # The Fun for Main()
app = QApplication(sys.argv)
Window = GSPP()
Window.setWindowTitle("Main")
qr = Window.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
Window.move(qr.topLeft())
Window.showMaximized()
app.processEvents()
sys.exit(app.exec_())

Python: how to get coordinates on mouse click using matplotlib.canvas

I am writing a class to process images. In that class, I want to define a method that can allow me to return coordinates of the mouse clicks. I can get the coordinates as an attribute but if I call the method to return the coordinates, I get an empty tuple
Here is the code:
import cv2
import matplotlib.pyplot as plt
class TestClass():
def __init__(self):
self.fname = 'image.jpg'
self.img = cv2.imread(self.fname)
self.point = ()
def getCoord(self):
fig = plt.figure()
ax = fig.add_subplot(111)
plt.imshow(self.img)
cid = fig.canvas.mpl_connect('button_press_event', self.__onclick__)
return self.point
def __onclick__(self,click):
self.point = (click.xdata,click.ydata)
return self.point
Your code works for me, as long as I insert plt.show() after mpl_connect in getCoord:
def getCoord(self):
fig = plt.figure()
ax = fig.add_subplot(111)
plt.imshow(self.img)
cid = fig.canvas.mpl_connect('button_press_event', self.__onclick__)
plt.show()
return self.point

Dynamically adding a vertical line to matplotlib plot

I'm trying to add vertical lines to a matplotlib plot dynmically when a user clicks on a particular point.
import matplotlib.pyplot as plt
import matplotlib.dates as mdate
class PointPicker(object):
def __init__(self,dates,values):
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111)
self.lines2d, = self.ax.plot_date(dates, values, linestyle='-',picker=5)
self.fig.canvas.mpl_connect('pick_event', self.onpick)
self.fig.canvas.mpl_connect('key_press_event', self.onpress)
def onpress(self, event):
"""define some key press events"""
if event.key.lower() == 'q':
sys.exit()
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
print self.ax.axvline(x=x, visible=True)
x = mdate.num2date(x)
print x,y,type(x)
if __name__ == '__main__':
import numpy as np
import datetime
dates=[datetime.datetime.now()+i*datetime.timedelta(days=1) for i in range(100)]
values = np.random.random(100)
plt.ion()
p = PointPicker(dates,values)
plt.show()
Here's an (almost) working example. When I click a point, the onpick method is indeed called and the data seems to be correct, but no vertical line shows up. What do I need to do to get the vertical line to show up?
Thanks
You need to update the canvas drawing (self.fig.canvas.draw()):
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
L = self.ax.axvline(x=x)
self.fig.canvas.draw()

Categories