Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4723f8a

Browse files
Joschua-ConradsmartlixxtimhoffmQuLogic
authored
Example: Cursor widget with text (matplotlib#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 <[email protected]> * Unite figure and axes creation Follows suggestion of timhoffm Co-authored-by: Tim Hoffmann <[email protected]> * Remove comments on sample data creation Suggested by timhoffm Co-authored-by: Tim Hoffmann <[email protected]> * 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 <[email protected]> * Restructured nested if condition Co-authored-by: Elliott Sales de Andrade <[email protected]> * Another braces removal Co-authored-by: Elliott Sales de Andrade <[email protected]> * Apply new string format syntax to exception string Co-authored-by: Elliott Sales de Andrade <[email protected]> * Simplified search for plot datat index matching mouse cursor position Co-authored-by: Elliott Sales de Andrade <[email protected]> * Fix typo in docstring Co-authored-by: Elliott Sales de Andrade <[email protected]> * Fix typo in docstring Co-authored-by: Elliott Sales de Andrade <[email protected]> * Added linefeed in docstring Co-authored-by: Elliott Sales de Andrade <[email protected]> * Fix typo in docstring Co-authored-by: Elliott Sales de Andrade <[email protected]> * Renamed setpos to set_position Co-authored-by: Elliott Sales de Andrade <[email protected]> * Fixed all misspellings of "background" Co-authored-by: Elliott Sales de Andrade <[email protected]> * Nicer explanation in comment about relating passed color arguments to color of graphical elements Co-authored-by: Elliott Sales de Andrade <[email protected]> * Fixed bug introduced with f93091e * Fixed "overwritten" typo in docstrings Co-authored-by: Elliott Sales de Andrade <[email protected]> * Added linefeeds in docstring Co-authored-by: Elliott Sales de Andrade <[email protected]> * Fixed "cooridnates" typo Co-authored-by: Elliott Sales de Andrade <[email protected]> * Shortened comment Co-authored-by: Elliott Sales de Andrade <[email protected]> * 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 <[email protected]> * Removed unused attribute self.textdata Co-authored-by: Elliott Sales de Andrade <[email protected]> * 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 <[email protected]> Co-authored-by: Joschua Conrad <[email protected]> Co-authored-by: Xianxiang Li <[email protected]> Co-authored-by: Tim Hoffmann <[email protected]> Co-authored-by: Elliott Sales de Andrade <[email protected]>
1 parent 1f1eab9 commit 4723f8a

File tree

1 file changed

+340
-0
lines changed

1 file changed

+340
-0
lines changed
Lines 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)