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?

- 785
- 2
- 8
- 24
-
Please check the below link and see if it resolves your issue. The link provide a function snaptocursor which looks similar to what you are looking for. https://matplotlib.org/3.1.1/gallery/misc/cursor_demo_sgskip.html – Anupam Chaplot Feb 15 '20 at 17:11
-
@AnupamChaplot "It uses Matplotlib to draw the cursor and may be a slow since this requires redrawing the figure with every mouse move." I have about 16 plots with 10000 points EACH on the graph, so with redrawing this would be rather slow. – Pygmalion Feb 15 '20 at 21:18
-
If you don't want to redraw anything visually (why ask for that then?), you can manipulate what is shown in the toolbar as shown in https://matplotlib.org/3.1.1/gallery/images_contours_and_fields/image_zcoord.html – ImportanceOfBeingErnest Feb 16 '20 at 16:20
-
@ImportanceOfBeingErnest I don't understand your suggestion. But imagine this: you have 16 line plots and each of them has a distinct peak. You want to know the exact coordinates of the peak of one plot without peeking into the data. You can never put the cursor exactly on the point, so this is highly imprecise. So programs like Origin have an option to show the exact coordinates of the closest point to the current cursor position. – Pygmalion Feb 16 '20 at 20:56
-
1Yes, that's what [cursor_demo_sgskip](https://matplotlib.org/3.1.1/gallery/misc/cursor_demo_sgskip.html) does. But if you don't want to draw the cursor, you can use the calculations from that example and instead display the resulting number in the toolbar, as shown in [image_zcoord](https://matplotlib.org/3.1.1/gallery/images_contours_and_fields/image_zcoord.html) – ImportanceOfBeingErnest Feb 16 '20 at 21:02
-
@ImportanceOfBeingErnest +1 thanks to pointing to `format_coord` function. To finalize search for the closest point was however extremely complicated, since I have so many points and several plots, plus I had to find axis ratio. Unpractical, unless included into the matplotlib library. Could you provide your comment as a solution? – Pygmalion Feb 17 '20 at 16:18
4 Answers
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()

- 5,759
- 1
- 14
- 34
-
1This will of course work flawlessly only if x:y visual scale equals 1. Any idea about this part of the problem, except for re-scaling `points` each time plot is zoomed? – Pygmalion Feb 22 '20 at 22:20
-
Changing aspect ratio requires to change metrics of how distance is measured in ckdtrees. It seems like using custom metrics on ckdtrees is not supported. Therefore you must keep `ckdtree.data` as realistic points with scale = 1. Your `points` can be rescaled and there is no issue if you need to access their indices only. – mathfux Feb 22 '20 at 22:35
-
Thanks. Do you know, by any chance, if there is a way to easily access the reuw scale ratio for axes in `matplotlib`? What I found on web was extremely complicated. – Pygmalion Feb 23 '20 at 10:06
-
IMHO the best solution for my problem would be to include that as an option into `matplotlib` library. After all, the library has rescalled point positions somewhere - after all, it is drawing them up in the plot! – Pygmalion Feb 23 '20 at 10:08
-
You might like to try `set_aspect`: https://matplotlib.org/3.1.3/api/_as_gen/matplotlib.axes.Axes.set_aspect.html – mathfux Feb 23 '20 at 10:12
-
-
+1 for pointing to `format_coord`. The rest I solved different way, with the help of `https://stackoverflow.com/questions/41597177/get-aspect-ratio-of-axes`. Solving this would qualify as a very useful improvement to `matplotlib` library, if I only knew where to post suggestions... – Pygmalion Feb 29 '20 at 18:25
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_()

- 314
- 1
- 8
-
This is a very solid solution, thanks a lot! I don't understand the `axis=1` part in the Snapper class, `distances = np.linalg.norm(self.data - pos, axis=1)`. – Guimoute Jun 11 '20 at 16:45
-
1(self.data - pos) is a n x 2 matrix storing storing n 2-dimensional position vectors. The axis=1 parameter tells numpy to compute the norm for each row of the matrix, returning the Euclidean lengths of these position vectors. axis=0 would work column-by-column and axis=None would return the matrix norm. Have a look at https://stackoverflow.com/a/52491249/5351066 for a nice visual explanation. – Alexander Rossmanith Jun 13 '20 at 06:58
-
Ah I see, thank you for the link. So `axis=1` means `distances` is now an array of length n containing `[sqrt(dx0² + dy0²), sqrt(dx1² + dy1²), sqrt(dx2² + dy2²), ...]` while `axis=0` would create a meaningless object of length 2 containing `[sqrt(dx0² + dx1² + dx2² + ...), sqrt(dy0²+ dy1² + dy2² + ...)]` – Guimoute Jun 13 '20 at 14:01
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()

- 9,588
- 5
- 28
- 50
-
I would probably be able to adapt the code to my situation (16 plots with 10000 points each), but the idea was that the coordinates of the dot are printed on, say, navigation toolbar. Is that possible? – Pygmalion Feb 15 '20 at 21:20
Another possibility is to use the picking support axes already have. See this section in the event handling docs.
Jim

- 474
- 5
- 17