Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4723f8a

Browse filesBrowse files
Joschua-ConradsmartlixxtimhoffmQuLogic
authored
Example: Cursor widget with text (#20019)
* Base version with the new cursor and one example. * Extended example with more subexamples showing a minimum __init__ call and trouble with the dataaxis parameter * Added notice for problems occurring when using non biunique plots and dataaxis=y * Matched docstring such that Sphinx output looks like as for the other widgets. Had to modify default format string therefore. * Added whats new message * Code should pass flake8 test now. * Cursor sticks to plotted line now * fixed indention * Added test * Maybe flake8 test passes now. Had problems with backslashes escaping multiline function calls. * Moved new text cursor class definition to its example file. This leaves widgets.py unchanged in comparison to master branch * Also removed the unittest of the new TextCursor class, as it now is an example * Fixed linter errors in example sourcefile * Added reST explanation for new example. * Built the docs and removed two docstring mistakes. * Docstring and code format changes The hint on the missing cursor in example's preview is now a note in the docstring. FIxed format of multiline examples of Cursor instantiation * What's new description now describes a new example instead of a new feature * Removed what's new note * Fix typo in docstring Change suggested by smartlixx Co-authored-by: Xianxiang Li <smartlixx@gmail.com> * Unite figure and axes creation Follows suggestion of timhoffm Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Remove comments on sample data creation Suggested by timhoffm Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Renamed "lin" to "line" Suggested by timhoffm * Unpacking tuple returned by "plot()" now the moment it is returned. Suggested by timhoffm * Moved example with non-biunique functions to a new example section * Referencing the cross hair cursor demo now * Renamed the new cursor from text_cursor to annotated_cursor File and class name changed * Skipping redrawing the cursor now when moving mouse between two plot points * Remove braces Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Restructured nested if condition Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Another braces removal Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Apply new string format syntax to exception string Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Simplified search for plot datat index matching mouse cursor position Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Fix typo in docstring Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Fix typo in docstring Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Added linefeed in docstring Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Fix typo in docstring Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Renamed setpos to set_position Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Fixed all misspellings of "background" Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Nicer explanation in comment about relating passed color arguments to color of graphical elements Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Fixed bug introduced with f93091e * Fixed "overwritten" typo in docstrings Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Added linefeeds in docstring Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Fixed "cooridnates" typo Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Shortened comment Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Heavily improve readibility of docstrings Some stuff rephrased Fixed typos Changed default argument for text offset from list to tuple Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Removed unused attribute self.textdata Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> * Fixed linting error * Added axes title The title is a ax.set_title("Cursor Tracking [x|y] Position") call Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com> Co-authored-by: Joschua Conrad <joschua.conrad@uni-ulm.de> Co-authored-by: Xianxiang Li <smartlixx@gmail.com> Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
1 parent 1f1eab9 commit 4723f8a
Copy full SHA for 4723f8a

File tree

Expand file treeCollapse file tree

1 file changed

+340
-0
lines changed
Filter options
Expand file treeCollapse file tree

1 file changed

+340
-0
lines changed

‎examples/widgets/annotated_cursor.py

Copy file name to clipboard
+340Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
"""
2+
================
3+
Annotated Cursor
4+
================
5+
6+
Display a data cursor including a text box, which shows the plot point close
7+
to the mouse pointer.
8+
9+
The new cursor inherits from `~matplotlib.widgets.Cursor` and demonstrates the
10+
creation of new widgets and their event callbacks.
11+
12+
See also the :doc:`cross hair cursor
13+
</gallery/misc/cursor_demo>`, which implements a cursor tracking the plotted
14+
data, but without using inheritance and without displaying the currently
15+
tracked coordinates.
16+
17+
.. note::
18+
The figure related to this example does not show the cursor, because that
19+
figure is automatically created in a build queue, where the first mouse
20+
movement, which triggers the cursor creation, is missing.
21+
22+
"""
23+
from matplotlib.widgets import Cursor
24+
import numpy as np
25+
import matplotlib.pyplot as plt
26+
27+
28+
class AnnotatedCursor(Cursor):
29+
"""
30+
A crosshair cursor like `~matplotlib.widgets.Cursor` with a text showing \
31+
the current coordinates.
32+
33+
For the cursor to remain responsive you must keep a reference to it.
34+
The data of the axis specified as *dataaxis* must be in ascending
35+
order. Otherwise, the `numpy.searchsorted` call might fail and the text
36+
disappears. You can satisfy the requirement by sorting the data you plot.
37+
Usually the data is already sorted (if it was created e.g. using
38+
`numpy.linspace`), but e.g. scatter plots might cause this problem.
39+
The cursor sticks to the plotted line.
40+
41+
Parameters
42+
----------
43+
line : `matplotlib.lines.Line2D`
44+
The plot line from which the data coordinates are displayed.
45+
46+
numberformat : `python format string <https://docs.python.org/3/\
47+
library/string.html#formatstrings>`_, optional, default: "{0:.4g};{1:.4g}"
48+
The displayed text is created by calling *format()* on this string
49+
with the two coordinates.
50+
51+
offset : (float, float) default: (5, 5)
52+
The offset in display (pixel) coordinates of the text position
53+
relative to the cross hair.
54+
55+
dataaxis : {"x", "y"}, optional, default: "x"
56+
If "x" is specified, the vertical cursor line sticks to the mouse
57+
pointer. The horizontal cursor line sticks to *line*
58+
at that x value. The text shows the data coordinates of *line*
59+
at the pointed x value. If you specify "y", it works in the opposite
60+
manner. But: For the "y" value, where the mouse points to, there might
61+
be multiple matching x values, if the plotted function is not biunique.
62+
Cursor and text coordinate will always refer to only one x value.
63+
So if you use the parameter value "y", ensure that your function is
64+
biunique.
65+
66+
Other Parameters
67+
----------------
68+
textprops : `matplotlib.text` properties as dictionay
69+
Specifies the appearance of the rendered text object.
70+
71+
**cursorargs : `matplotlib.widgets.Cursor` properties
72+
Arguments passed to the internal `~matplotlib.widgets.Cursor` instance.
73+
The `matplotlib.axes.Axes` argument is mandatory! The parameter
74+
*useblit* can be set to *True* in order to achieve faster rendering.
75+
76+
"""
77+
78+
def __init__(self, line, numberformat="{0:.4g};{1:.4g}", offset=(5, 5),
79+
dataaxis='x', textprops={}, **cursorargs):
80+
# The line object, for which the coordinates are displayed
81+
self.line = line
82+
# The format string, on which .format() is called for creating the text
83+
self.numberformat = numberformat
84+
# Text position offset
85+
self.offset = np.array(offset)
86+
# The axis in which the cursor position is looked up
87+
self.dataaxis = dataaxis
88+
89+
# First call baseclass constructor.
90+
# Draws cursor and remembers background for blitting.
91+
# Saves ax as class attribute.
92+
super().__init__(**cursorargs)
93+
94+
# Default value for position of text.
95+
self.set_position(self.line.get_xdata()[0], self.line.get_ydata()[0])
96+
# Create invisible animated text
97+
self.text = self.ax.text(
98+
self.ax.get_xbound()[0],
99+
self.ax.get_ybound()[0],
100+
"0, 0",
101+
animated=bool(self.useblit),
102+
visible=False, **textprops)
103+
# The position at which the cursor was last drawn
104+
self.lastdrawnplotpoint = None
105+
106+
def onmove(self, event):
107+
"""
108+
Overridden draw callback for cursor. Called when moving the mouse.
109+
"""
110+
111+
# Leave method under the same conditions as in overridden method
112+
if self.ignore(event):
113+
self.lastdrawnplotpoint = None
114+
return
115+
if not self.canvas.widgetlock.available(self):
116+
self.lastdrawnplotpoint = None
117+
return
118+
119+
# If the mouse left drawable area, we now make the text invisible.
120+
# Baseclass will redraw complete canvas after, which makes both text
121+
# and cursor disappear.
122+
if event.inaxes != self.ax:
123+
self.lastdrawnplotpoint = None
124+
self.text.set_visible(False)
125+
super().onmove(event)
126+
return
127+
128+
# Get the coordinates, which should be displayed as text,
129+
# if the event coordinates are valid.
130+
plotpoint = None
131+
if event.xdata is not None and event.ydata is not None:
132+
# Get plot point related to current x position.
133+
# These coordinates are displayed in text.
134+
plotpoint = self.set_position(event.xdata, event.ydata)
135+
# Modify event, such that the cursor is displayed on the
136+
# plotted line, not at the mouse pointer,
137+
# if the returned plot point is valid
138+
if plotpoint is not None:
139+
event.xdata = plotpoint[0]
140+
event.ydata = plotpoint[1]
141+
142+
# If the plotpoint is given, compare to last drawn plotpoint and
143+
# return if they are the same.
144+
# Skip even the call of the base class, because this would restore the
145+
# background, draw the cursor lines and would leave us the job to
146+
# re-draw the text.
147+
if plotpoint is not None and plotpoint == self.lastdrawnplotpoint:
148+
return
149+
150+
# Baseclass redraws canvas and cursor. Due to blitting,
151+
# the added text is removed in this call, because the
152+
# background is redrawn.
153+
super().onmove(event)
154+
155+
# Check if the display of text is still necessary.
156+
# If not, just return.
157+
# This behaviour is also cloned from the base class.
158+
if not self.get_active() or not self.visible:
159+
return
160+
161+
# Draw the widget, if event coordinates are valid.
162+
if plotpoint is not None:
163+
# Update position and displayed text.
164+
# Position: Where the event occured.
165+
# Text: Determined by set_position() method earlier
166+
# Position is transformed to pixel coordinates,
167+
# an offset is added there and this is transformed back.
168+
temp = [event.xdata, event.ydata]
169+
temp = self.ax.transData.transform(temp)
170+
temp = temp + self.offset
171+
temp = self.ax.transData.inverted().transform(temp)
172+
self.text.set_position(temp)
173+
self.text.set_text(self.numberformat.format(*plotpoint))
174+
self.text.set_visible(self.visible)
175+
176+
# Tell base class, that we have drawn something.
177+
# Baseclass needs to know, that it needs to restore a clean
178+
# background, if the cursor leaves our figure context.
179+
self.needclear = True
180+
181+
# Remember the recently drawn cursor position, so events for the
182+
# same position (mouse moves slightly between two plot points)
183+
# can be skipped
184+
self.lastdrawnplotpoint = plotpoint
185+
# otherwise, make text invisible
186+
else:
187+
self.text.set_visible(False)
188+
189+
# Draw changes. Cannot use _update method of baseclass,
190+
# because it would first restore the background, which
191+
# is done already and is not necessary.
192+
if self.useblit:
193+
self.ax.draw_artist(self.text)
194+
self.canvas.blit(self.ax.bbox)
195+
else:
196+
# If blitting is deactivated, the overridden _update call made
197+
# by the base class immediately returned.
198+
# We still have to draw the changes.
199+
self.canvas.draw_idle()
200+
201+
def set_position(self, xpos, ypos):
202+
"""
203+
Finds the coordinates, which have to be shown in text.
204+
205+
The behaviour depends on the *dataaxis* attribute. Function looks
206+
up the matching plot coordinate for the given mouse position.
207+
208+
Parameters
209+
----------
210+
xpos : float
211+
The current x position of the cursor in data coordinates.
212+
Important if *dataaxis* is set to 'x'.
213+
ypos : float
214+
The current y position of the cursor in data coordinates.
215+
Important if *dataaxis* is set to 'y'.
216+
217+
Returns
218+
-------
219+
ret : {2D array-like, None}
220+
The coordinates which should be displayed.
221+
*None* is the fallback value.
222+
"""
223+
224+
# Get plot line data
225+
xdata = self.line.get_xdata()
226+
ydata = self.line.get_ydata()
227+
228+
# The dataaxis attribute decides, in which axis we look up which cursor
229+
# coordinate.
230+
if self.dataaxis == 'x':
231+
pos = xpos
232+
data = xdata
233+
lim = self.ax.get_xlim()
234+
elif self.dataaxis == 'y':
235+
pos = ypos
236+
data = ydata
237+
lim = self.ax.get_ylim()
238+
else:
239+
raise ValueError(f"The data axis specifier {self.dataaxis} should "
240+
f"be 'x' or 'y'")
241+
242+
# If position is valid and in valid plot data range.
243+
if pos is not None and lim[0] <= pos <= lim[-1]:
244+
# Find closest x value in sorted x vector.
245+
# This requires the plotted data to be sorted.
246+
index = np.searchsorted(data, pos)
247+
# Return none, if this index is out of range.
248+
if index < 0 or index >= len(data):
249+
return None
250+
# Return plot point as tuple.
251+
return (xdata[index], ydata[index])
252+
253+
# Return none if there is no good related point for this x position.
254+
return None
255+
256+
def clear(self, event):
257+
"""
258+
Overridden clear callback for cursor, called before drawing the figure.
259+
"""
260+
261+
# The base class saves the clean background for blitting.
262+
# Text and cursor are invisible,
263+
# until the first mouse move event occurs.
264+
super().clear(event)
265+
if self.ignore(event):
266+
return
267+
self.text.set_visible(False)
268+
269+
def _update(self):
270+
"""
271+
Overridden method for either blitting or drawing the widget canvas.
272+
273+
Passes call to base class if blitting is activated, only.
274+
In other cases, one draw_idle call is enough, which is placed
275+
explicitly in this class (see *onmove()*).
276+
In that case, `~matplotlib.widgets.Cursor` is not supposed to draw
277+
something using this method.
278+
"""
279+
280+
if self.useblit:
281+
super()._update()
282+
283+
284+
fig, ax = plt.subplots(figsize=(8, 6))
285+
ax.set_title("Cursor Tracking x Position")
286+
287+
x = np.linspace(-5, 5, 1000)
288+
y = x**2
289+
290+
line, = ax.plot(x, y)
291+
ax.set_xlim(-5, 5)
292+
ax.set_ylim(0, 25)
293+
294+
# A minimum call
295+
# Set useblit=True on most backends for enhanced performance
296+
# and pass the ax parameter to the Cursor base class.
297+
# cursor = AnnotatedCursor(line=lin[0], ax=ax, useblit=True)
298+
299+
# A more advanced call. Properties for text and lines are passed.
300+
# Watch the passed color names and the color of cursor line and text, to
301+
# relate the passed options to graphical elements.
302+
# The dataaxis parameter is still the default.
303+
cursor = AnnotatedCursor(
304+
line=line,
305+
numberformat="{0:.2f}\n{1:.2f}",
306+
dataaxis='x', offset=[10, 10],
307+
textprops={'color': 'blue', 'fontweight': 'bold'},
308+
ax=ax,
309+
useblit=True,
310+
color='red',
311+
linewidth=2)
312+
313+
plt.show()
314+
315+
###############################################################################
316+
# Trouble with non-biunique functions
317+
# -----------------------------------
318+
# A call demonstrating problems with the *dataaxis=y* parameter.
319+
# The text now looks up the matching x value for the current cursor y position
320+
# instead of vice versa. Hover your cursor to y=4. There are two x values
321+
# producing this y value: -2 and 2. The function is only unique,
322+
# but not biunique. Only one value is shown in the text.
323+
324+
fig, ax = plt.subplots(figsize=(8, 6))
325+
ax.set_title("Cursor Tracking y Position")
326+
327+
line, = ax.plot(x, y)
328+
ax.set_xlim(-5, 5)
329+
ax.set_ylim(0, 25)
330+
331+
cursor = AnnotatedCursor(
332+
line=line,
333+
numberformat="{0:.2f}\n{1:.2f}",
334+
dataaxis='y', offset=[10, 10],
335+
textprops={'color': 'blue', 'fontweight': 'bold'},
336+
ax=ax,
337+
useblit=True,
338+
color='red', linewidth=2)
339+
340+
plt.show()

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.