Skip to content

Commit 260aa45

Browse files
committed
Rewrite cursor example to include speedup possibilities
1 parent 2c5c351 commit 260aa45

File tree

2 files changed

+219
-91
lines changed

2 files changed

+219
-91
lines changed

examples/misc/cursor_demo.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
=================
3+
Cross hair cursor
4+
=================
5+
6+
This example adds a cross hair as a data cursor. The cross hair is
7+
implemented as regular line objects that are updated on mouse move.
8+
9+
We show three implementations:
10+
11+
1) A simple cursor implementation that redraws the figure on every mouse move.
12+
This is a bit slow and you may notice some lag of the cross hair movement.
13+
2) A cursor that uses blitting for speedup of the rendering.
14+
3) A cursor that snaps to data points.
15+
16+
Faster cursoring is possible using native GUI drawing, as in
17+
:doc:`/gallery/user_interfaces/wxcursor_demo_sgskip`.
18+
19+
The mpldatacursor__ and mplcursors__ third-party packages can be used to
20+
achieve a similar effect.
21+
22+
__ https://github.com/joferkington/mpldatacursor
23+
__ https://github.com/anntzer/mplcursors
24+
"""
25+
26+
import matplotlib.pyplot as plt
27+
import numpy as np
28+
29+
30+
class Cursor:
31+
"""
32+
A cross hair cursor.
33+
"""
34+
def __init__(self, ax):
35+
self.ax = ax
36+
self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
37+
self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
38+
# text location in axes coordinates
39+
self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
40+
41+
def set_cross_hair_visible(self, visible):
42+
need_redraw = self.horizontal_line.get_visible() != visible
43+
self.horizontal_line.set_visible(visible)
44+
self.vertical_line.set_visible(visible)
45+
self.text.set_visible(visible)
46+
return need_redraw
47+
48+
def on_mouse_move(self, event):
49+
if not event.inaxes:
50+
need_redraw = self.set_cross_hair_visible(False)
51+
if need_redraw:
52+
self.ax.figure.canvas.draw()
53+
else:
54+
self.set_cross_hair_visible(True)
55+
x, y = event.xdata, event.ydata
56+
# update the line positions
57+
self.horizontal_line.set_ydata(y)
58+
self.vertical_line.set_xdata(x)
59+
self.text.set_text('x=%1.2f, y=%1.2f' % (x, y))
60+
self.ax.figure.canvas.draw()
61+
62+
63+
x = np.arange(0, 1, 0.01)
64+
y = np.sin(2 * 2 * np.pi * x)
65+
66+
fig, ax = plt.subplots()
67+
ax.set_title('Simple cursor')
68+
ax.plot(x, y, 'o')
69+
cursor = Cursor(ax)
70+
fig.canvas.mpl_connect('motion_notify_event', cursor.on_mouse_move)
71+
72+
73+
##############################################################################
74+
# Faster redrawing using blitting
75+
# """""""""""""""""""""""""""""""
76+
# This technique stores the rendered plot as a background image. Only the
77+
# changed artists (cross hair lines and text) are rendered anew. They are
78+
# combined with the background using blitting.
79+
#
80+
# This technique is significantly faster. It requires a bit more setup because
81+
# the background has to be stored without the cross hair lines (see
82+
# ``create_new_background()``). Additionally, a new background has to be
83+
# created whenever the figure changes. This is achieved by connecting to the
84+
# ``'draw_event'``.
85+
86+
class BlittedCursor:
87+
"""
88+
A cross hair cursor using blitting for faster redraw.
89+
"""
90+
def __init__(self, ax):
91+
self.ax = ax
92+
self.background = None
93+
self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
94+
self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
95+
# text location in axes coordinates
96+
self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
97+
self._creating_background = False
98+
ax.figure.canvas.mpl_connect('draw_event', self.on_draw)
99+
100+
def on_draw(self, event):
101+
self.create_new_background()
102+
103+
def set_cross_hair_visible(self, visible):
104+
need_redraw = self.horizontal_line.get_visible() != visible
105+
self.horizontal_line.set_visible(visible)
106+
self.vertical_line.set_visible(visible)
107+
self.text.set_visible(visible)
108+
return need_redraw
109+
110+
def create_new_background(self):
111+
if self._creating_background:
112+
# discard calls triggered from within this function
113+
return
114+
self._creating_background = True
115+
self.set_cross_hair_visible(False)
116+
self.ax.figure.canvas.draw()
117+
self.background = self.ax.figure.canvas.copy_from_bbox(self.ax.bbox)
118+
self.set_cross_hair_visible(True)
119+
self._creating_background = False
120+
121+
def on_mouse_move(self, event):
122+
if self.background is None:
123+
self.create_new_background()
124+
if not event.inaxes:
125+
need_redraw = self.set_cross_hair_visible(False)
126+
if need_redraw:
127+
self.ax.figure.canvas.restore_region(self.background)
128+
self.ax.figure.canvas.blit(self.ax.bbox)
129+
else:
130+
self.set_cross_hair_visible(True)
131+
# update the line positions
132+
x, y = event.xdata, event.ydata
133+
self.horizontal_line.set_ydata(y)
134+
self.vertical_line.set_xdata(x)
135+
self.text.set_text('x=%1.2f, y=%1.2f' % (x, y))
136+
137+
self.ax.figure.canvas.restore_region(self.background)
138+
self.ax.draw_artist(self.horizontal_line)
139+
self.ax.draw_artist(self.vertical_line)
140+
self.ax.draw_artist(self.text)
141+
self.ax.figure.canvas.blit(self.ax.bbox)
142+
143+
144+
x = np.arange(0, 1, 0.01)
145+
y = np.sin(2 * 2 * np.pi * x)
146+
147+
fig, ax = plt.subplots()
148+
ax.set_title('Blitted cursor')
149+
ax.plot(x, y, 'o')
150+
blitted_cursor = BlittedCursor(ax)
151+
fig.canvas.mpl_connect('motion_notify_event', blitted_cursor.on_mouse_move)
152+
153+
154+
##############################################################################
155+
# Snapping to data points
156+
# """""""""""""""""""""""
157+
# The following cursor snaps its position to the data points of a `.Line2D`
158+
# object.
159+
#
160+
# To save unnecessary redraws, the index of the last indicated data point is
161+
# saved in ``self._last_index``. A redraw is only triggered when the mouse
162+
# moves far enough so that another data point must be selected. This reduces
163+
# the lag due to many redraws. Of course, blitting could still be added on top
164+
# for additional speedup.
165+
166+
class SnappingCursor:
167+
"""
168+
A cross hair cursor that snaps to the data point of a line, which is
169+
closest to the *x* position of the cursor.
170+
171+
For simplicity, this assumes that *x* values of the data is sorted.
172+
"""
173+
def __init__(self, ax, line):
174+
self.ax = ax
175+
self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
176+
self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
177+
self.x, self.y = line.get_data()
178+
self._last_index = None
179+
# text location in axes coords
180+
self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
181+
182+
def set_cross_hair_visible(self, visible):
183+
need_redraw = self.horizontal_line.get_visible() != visible
184+
self.horizontal_line.set_visible(visible)
185+
self.vertical_line.set_visible(visible)
186+
self.text.set_visible(visible)
187+
return need_redraw
188+
189+
def on_mouse_move(self, event):
190+
if not event.inaxes:
191+
self._last_index = None
192+
need_redraw = self.set_cross_hair_visible(False)
193+
if need_redraw:
194+
self.ax.figure.canvas.draw()
195+
else:
196+
self.set_cross_hair_visible(True)
197+
x, y = event.xdata, event.ydata
198+
index = min(np.searchsorted(self.x, x), len(self.x) - 1)
199+
if index == self._last_index:
200+
return # still on the same data point. Nothing to do.
201+
self._last_index = index
202+
x = self.x[index]
203+
y = self.y[index]
204+
# update the line positions
205+
self.horizontal_line.set_ydata(y)
206+
self.vertical_line.set_xdata(x)
207+
self.text.set_text('x=%1.2f, y=%1.2f' % (x, y))
208+
self.ax.figure.canvas.draw()
209+
210+
211+
x = np.arange(0, 1, 0.01)
212+
y = np.sin(2 * 2 * np.pi * x)
213+
214+
fig, ax = plt.subplots()
215+
ax.set_title('Snapping cursor')
216+
line, = ax.plot(x, y, 'o')
217+
snap_cursor = SnappingCursor(ax, line)
218+
fig.canvas.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move)
219+
plt.show()

examples/misc/cursor_demo_sgskip.py

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)