I'm using the DataCursor code from unutbu to show annotations on a matplotlib figure when I click on a data point. (I can't find the link to this for the life of me, and the one I used to cite in my code doesn't point to the right place...) The problem is, I want them to go away when I click outside the plot.
def fmt(x, xlabel, y, ylabel)
return xlabel+': {x:0.0f}\n'.format(x = x)+ylabel+': {y:0.3f}'.format(y = y)
class DataCursor(object):
"""A data cursor widget that displays the x,y of a matplotlib artist/plot when it is selected"""
def annotate(self, ax)
""" Draws and hides the annotations box for the given axis "ax". """
annotation = ax.annotate(self.formatter, xy = (0,0), ha = 'right',
xytext = self.offsets, textcoords = 'offset points', va = 'bottom',
bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'))
annotation.set_visible(False)
return annotation
def __init__(self, artists, x, xlabel, y, ylabel, tolerance = 5, offsets = (-20,20), formatter = fmt, display_all = False):
"""Create the data cursor and connect it to the relevant figure.
"artists" is the matplotlib artist or sequence of artists that will be selected.
"tolerance" is the radius (in points) that the mouse click must be within to select the artist.
"offsets" is a tuple of (x,y) offsets in points from the selected point to the displayed annotation box.
"formatter" is a callback function which takes 2 numeric arguments and returns a string.
"display_all" controls whether more than one annotation box will be shown if there are multiple axes. Only one will be shown per axis, regardless.
"""
self._points = np.column_stack((x,y))
self.xlabel = xlabel
self.ylabel = ylabel
self.formatter = formatter
self.offsets = offsets
self.display_all = display_all
if not cbook.iterable(artists):
artists = [artists]
self.artists = artists
self.axes = tuple(set(art.axes for art in self.artists))
self.figures = tuple(set(ax.figure for ax in self.axes))
self.annotations = {}
for ax in self.axes:
self.annotations[ax] = self.annotate(ax)
for artist in self.artists:
artist.set_picker(tolerance)
for fig in self.figures:
fig.canvas.mpl_connect('pick_event', self)
def snap(self, x, y):
""" Return the value of in self._points closest to (x,y). """
values = self.points
idx = np.nanargmin(((values - (x,y))**2).sum(axis = -1)
return self._points[idx,:]
def __call__(self, event):
""" Intended to be called through "mpl_connect" above. """
x, y = event.mouseevent.xdata, event.mouseevent.ydata
annotation = self.annotations[event.artist.axes]
if x is None:
for ann in self.annotations.values():
ann.set_visible(False)
event.canvas.draw()
if x is not None:
if not self.display_all:
for ann in self.annotations.values():
ann.set_visible(False)
x, y = self.snap(x, y)
annotation.xy = x, y
annotation.set_text(self.formatter(x, self.xlabel, y, self.ylabel))
annotation.set_visible(True)
annotation.draggable(True)
event.canvas.draw()
According to documentation, I could use a function in artist.set_picker(tolerance) instead of tolerance. How does this work? The documentation doesn't do a great job of explaining this, and I'm having trouble finding examples.
The way the code works now, it will make the annotation disappear if I click outside the box, but within tolerance of a data point. This leaves me out of luck if there is no data within tolerance of the edge. How can I make it so that if I click anywhere in the gray area of the plot the annotation will disappear?
The code that I though would make it disappear:
def __call__(self, event):
""" Intended to be called through "mpl_connect" above. """
x, y = event.mouseevent.xdata, event.mouseevent.ydata
annotation = self.annotations[event.artist.axes]
if x is None:
for ann in self.annotations.values():
ann.set_visible(False)
event.canvas.draw()
Your problem is that your event-handling function only listens to 'pick_event' which are inherently associated with an artist (AFAIK).
To achieve what you want, you should add another listener that handles 'button_press_event'
This is untested code, but it should give you an idea:
# in the __init__ code, add:
fig.canvas.mpl_connect('button_press_event', self.clearAnn)
# in the class body:
def clearAnn(self, event):
if event.inaxes is None: # event.inaxes returns None if you click outside of the plot area, i.e. in the grey part surrounding the axes
for ann in self.annotations.values():
ann.set_visible(False)
event.canvas.draw()
Related
I have a class which sets up a matplotlib figure with the given data and by default it records clicks on the plot. It records every mouseclick, so zooming always add a new point. To prevent that I want to create a toggle button on the mpl toolbar to enable and disable click recording. Here is the code:
import numpy as np
import matplotlib
matplotlib.rcParams["toolbar"] = "toolmanager"
import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolToggleBase
from matplotlib.backend_bases import MouseButton
class EditPeak(object):
def __init__(self, x, y, x_extremal=None, y_extremal=None):
# setting up the plot
self.x = x
self.y = y
self.cid = None
self.figure = plt.figure()
self.press()
plt.plot(self.x, self.y, 'r')
self.x_extremal = x_extremal
self.y_extremal = y_extremal
if not len(self.x_extremal) == len(self.y_extremal):
raise ValueError('Data shapes are different.')
self.lins = plt.plot(self.x_extremal, self.y_extremal, 'ko', markersize=6, zorder=99)
plt.grid(alpha=0.7)
# adding the button
tm = self.figure.canvas.manager.toolmanager
tm.add_tool('Toggle recording', SelectButton)
self.figure.canvas.manager.toolbar.add_tool(tm.get_tool('Toggle recording'), "toolgroup")
plt.show()
def on_clicked(self, event):
""" Function to record and discard points on plot."""
ix, iy = event.xdata, event.ydata
if event.button is MouseButton.RIGHT:
ix, iy, idx = get_closest(ix, self.x_extremal, self.y_extremal)
self.x_extremal = np.delete(self.x_extremal, idx)
self.y_extremal = np.delete(self.y_extremal, idx)
elif event.button is MouseButton.LEFT:
ix, iy, idx = get_closest(ix, self.x, self.y)
self.x_extremal = np.append(self.x_extremal, ix)
self.y_extremal = np.append(self.y_extremal, iy)
else:
pass
plt.cla()
plt.plot(self.x, self.y, 'r')
self.lins = plt.plot(self.x_extremal, self.y_extremal, 'ko', markersize=6, zorder=99)
plt.grid(alpha=0.7)
plt.draw()
return
def press(self):
self.cid = self.figure.canvas.mpl_connect('button_press_event', self.on_clicked)
def release(self):
self.figure.canvas.mpl_disconnect(self.cid)
Where get_closest is the following:
def get_closest(x_value, x_array, y_array):
"""Finds the closest point in a graph to a given x_value, where distance is
measured with respect to x.
"""
idx = (np.abs(x_array - x_value)).argmin()
value = x_array[idx]
return value, y_array[idx], idx
This is the button on the toolbar.
class SelectButton(ToolToggleBase):
default_toggled = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def enable(self, event):
pass
def disable(self, event):
pass
I would like to activate a function when it's triggered, otherwise disable. My problem is that the enable and disable functions are defined in EditPeak class and I couldn't link them to the SelectButton class.
The easiest way you can try it (even though it doesn't really make sense with random values):
x = np.random.normal(0,1,100)
y = np.random.normal(0,1,100)
EditPeak(x, y, x[::2], y[::2])
Left click will add a new point to the closest x value in the given graph, right clicks will delete the closest.
My goal is to call EditPeak.press when the toggle button is on, and call EditPeak.release otherwise.
Any improvement in the code (including style, structure) is appreciated.
Right after:
tm.add_tool('Toggle recording', SelectButton)
you can do something like:
self.my_select_button = tm.get_tool('Toggle recording')
Now the EditPeak instance has a reference to the SelectButton instance created by add_tool that can be used in EditPeak.on_clicked. (The remainder is left as an exercise for the reader :-)
In MATLAB, one can use datacursormode to add annotation to a graph when user mouses over. Is there such thing in matplotlib? Or I need to write my own event using matplotlib.text.Annotation?
Late Edit / Shameless Plug: This is now available (with much more functionality) as mpldatacursor. Calling mpldatacursor.datacursor() will enable it for all matplotlib artists (including basic support for z-values in images, etc).
As far as I know, there isn't one already implemented, but it's not too hard to write something similar:
import matplotlib.pyplot as plt
class DataCursor(object):
text_template = 'x: %0.2f\ny: %0.2f'
x, y = 0.0, 0.0
xoffset, yoffset = -20, 20
text_template = 'x: %0.2f\ny: %0.2f'
def __init__(self, ax):
self.ax = ax
self.annotation = ax.annotate(self.text_template,
xy=(self.x, self.y), xytext=(self.xoffset, self.yoffset),
textcoords='offset points', ha='right', va='bottom',
bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5),
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')
)
self.annotation.set_visible(False)
def __call__(self, event):
self.event = event
# xdata, ydata = event.artist.get_data()
# self.x, self.y = xdata[event.ind], ydata[event.ind]
self.x, self.y = event.mouseevent.xdata, event.mouseevent.ydata
if self.x is not None:
self.annotation.xy = self.x, self.y
self.annotation.set_text(self.text_template % (self.x, self.y))
self.annotation.set_visible(True)
event.canvas.draw()
fig = plt.figure()
line, = plt.plot(range(10), 'ro-')
fig.canvas.mpl_connect('pick_event', DataCursor(plt.gca()))
line.set_picker(5) # Tolerance in points
As it seems like at least a few people are using this, I've added an updated version below.
The new version has a simpler usage and a lot more documentation (i.e. a tiny bit, at least).
Basically you'd use it similar to this:
plt.figure()
plt.subplot(2,1,1)
line1, = plt.plot(range(10), 'ro-')
plt.subplot(2,1,2)
line2, = plt.plot(range(10), 'bo-')
DataCursor([line1, line2])
plt.show()
The main differences are that a) there's no need to manually call line.set_picker(...), b) there's no need to manually call fig.canvas.mpl_connect, and c) this version handles multiple axes and multiple figures.
from matplotlib import cbook
class DataCursor(object):
"""A simple data cursor widget that displays the x,y location of a
matplotlib artist when it is selected."""
def __init__(self, artists, tolerance=5, offsets=(-20, 20),
template='x: %0.2f\ny: %0.2f', display_all=False):
"""Create the data cursor and connect it to the relevant figure.
"artists" is the matplotlib artist or sequence of artists that will be
selected.
"tolerance" is the radius (in points) that the mouse click must be
within to select the artist.
"offsets" is a tuple of (x,y) offsets in points from the selected
point to the displayed annotation box
"template" is the format string to be used. Note: For compatibility
with older versions of python, this uses the old-style (%)
formatting specification.
"display_all" controls whether more than one annotation box will
be shown if there are multiple axes. Only one will be shown
per-axis, regardless.
"""
self.template = template
self.offsets = offsets
self.display_all = display_all
if not cbook.iterable(artists):
artists = [artists]
self.artists = artists
self.axes = tuple(set(art.axes for art in self.artists))
self.figures = tuple(set(ax.figure for ax in self.axes))
self.annotations = {}
for ax in self.axes:
self.annotations[ax] = self.annotate(ax)
for artist in self.artists:
artist.set_picker(tolerance)
for fig in self.figures:
fig.canvas.mpl_connect('pick_event', self)
def annotate(self, ax):
"""Draws and hides the annotation box for the given axis "ax"."""
annotation = ax.annotate(self.template, xy=(0, 0), ha='right',
xytext=self.offsets, textcoords='offset points', va='bottom',
bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5),
arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')
)
annotation.set_visible(False)
return annotation
def __call__(self, event):
"""Intended to be called through "mpl_connect"."""
# Rather than trying to interpolate, just display the clicked coords
# This will only be called if it's within "tolerance", anyway.
x, y = event.mouseevent.xdata, event.mouseevent.ydata
annotation = self.annotations[event.artist.axes]
if x is not None:
if not self.display_all:
# Hide any other annotation boxes...
for ann in self.annotations.values():
ann.set_visible(False)
# Update the annotation in the current axis..
annotation.xy = x, y
annotation.set_text(self.template % (x, y))
annotation.set_visible(True)
event.canvas.draw()
if __name__ == '__main__':
import matplotlib.pyplot as plt
plt.figure()
plt.subplot(2,1,1)
line1, = plt.plot(range(10), 'ro-')
plt.subplot(2,1,2)
line2, = plt.plot(range(10), 'bo-')
DataCursor([line1, line2])
plt.show()
I would like to display an image in python and allow the user to click on a specific pixel. I then want to use the x and y coordinates to perform further calculations.
So far, I've been using the event picker:
def onpick1(event):
artist = event.artist
if isinstance(artist, AxesImage):
mouseevent = event.mouseevent
x = mouseevent.xdata
y = mouseevent.ydata
print x,y
xaxis = frame.shape[1]
yaxis = frame.shape[0]
fig = plt.figure(figsize=(6,9))
ax = fig.add_subplot(111)
line, = [ax.imshow(frame[::-1,:], cmap='jet', extent=(0,xaxis,0,yaxis), picker=5)]
fig.canvas.mpl_connect('pick_event', onpick1)
plt.show()
Now I would really like the function onpick1() to return x and y so I can use it after plt.show() to perform further calculations.
Any suggestions?
A good lesson with GUI programming is to go object oriented. Your problem right now is that you have an asynchronous callback and you want to keep its values. You should consider packing everything up together, like:
class MyClickableImage(object):
def __init__(self,frame):
self.x = None
self.y = None
self.frame = frame
self.fig = plt.figure(figsize=(6,9))
self.ax = self.fig.add_subplot(111)
xaxis = self.frame.shape[1]
yaxis = self.frame.shape[0]
self.im = ax.imshow(self.frame[::-1,:],
cmap='jet', extent=(0,xaxis,0,yaxis),
picker=5)
self.fig.canvas.mpl_connect('pick_event', self.onpick1)
plt.show()
# some other associated methods go here...
def onpick1(self,event):
artist = event.artist
if isinstance(artist, AxesImage):
mouseevent = event.mouseevent
self.x = mouseevent.xdata
self.y = mouseevent.ydata
Now when you click on a point, it will set the x and y attributes of your class. However, if you want to perform calculations using x and y you could simply have the onpick1 method perform those calculations.
I would like to track the coordinates of the mouse with respect to data coordinates on two axes simultaneously. I can track the mouse position with respect to one axis just fine. The problem is: when I add a second axis with twinx(), both Cursors report data coordinates with respect to the second axis only.
For example, my Cursors (fern and muffy) report the y-value is 7.93
Fern: (1597.63, 7.93)
Muffy: (1597.63, 7.93)
If I use:
inv = ax.transData.inverted()
x, y = inv.transform((event.x, event.y))
I get an IndexError.
So the question is: How can I modify the code to track the data coordinates with respect to both axes?
import numpy as np
import matplotlib.pyplot as plt
import logging
logger = logging.getLogger(__name__)
class Cursor(object):
def __init__(self, ax, name):
self.ax = ax
self.name = name
plt.connect('motion_notify_event', self)
def __call__(self, event):
x, y = event.xdata, event.ydata
ax = self.ax
# inv = ax.transData.inverted()
# x, y = inv.transform((event.x, event.y))
logger.debug('{n}: ({x:0.2f}, {y:0.2f})'.format(n=self.name,x=x,y=y))
logging.basicConfig(level=logging.DEBUG,
format='%(message)s',)
fig, ax = plt.subplots()
x = np.linspace(1000, 2000, 500)
y = 100*np.sin(20*np.pi*(x-1500)/2000.0)
fern = Cursor(ax, 'Fern')
ax.plot(x,y)
ax2 = ax.twinx()
z = x/200.0
muffy = Cursor(ax2, 'Muffy')
ax2.semilogy(x,z)
plt.show()
Due to the way that the call backs work, the event always returns in the top axes. You just need a bit of logic to check which if the event happens in the axes we want:
class Cursor(object):
def __init__(self, ax, x, y, name):
self.ax = ax
self.name = name
plt.connect('motion_notify_event', self)
def __call__(self, event):
if event.inaxes is None:
return
ax = self.ax
if ax != event.inaxes:
inv = ax.transData.inverted()
x, y = inv.transform(np.array((event.x, event.y)).reshape(1, 2)).ravel()
elif ax == event.inaxes:
x, y = event.xdata, event.ydata
else:
return
logger.debug('{n}: ({x:0.2f}, {y:0.2f})'.format(n=self.name,x=x,y=y))
This might be a subtle bug down in the transform stack (or this is the correct usage and it was by luck it worked with tuples before), but at any rate, this will make it work. The issue is that the code at line 1996 in transform.py expects to get a 2D ndarray back, but the identity transform just returns the tuple that get handed into it, which is what generates the errors.
You can track both axis coordinates with one cursor (or event handler) this way:
import numpy as np
import matplotlib.pyplot as plt
import logging
logger = logging.getLogger(__name__)
class Cursor(object):
def __init__(self):
plt.connect('motion_notify_event', self)
def __call__(self, event):
if event.inaxes is None:
return
x, y1 = ax1.transData.inverted().transform((event.x,event.y))
x, y2 = ax2.transData.inverted().transform((event.x,event.y))
logger.debug('(x,y1,y2)=({x:0.2f}, {y1:0.2f}, {y2:0.2f})'.format(x=x,y1=y1,y2=y2))
logging.basicConfig(level=logging.DEBUG,
format='%(message)s',)
fig, ax1 = plt.subplots()
x = np.linspace(1000, 2000, 500)
y = 100*np.sin(20*np.pi*(x-1500)/2000.0)
fern = Cursor()
ax1.plot(x,y)
ax2 = ax1.twinx()
z = x/200.0
ax2.plot(x,z)
plt.show()
(I got "too many indices" when I used ax2.semilogy(x,z) like the OP, but didn't work through that problem.)
The ax1.transData.inverted().transform((event.x,event.y)) code performs a transform from display to data coordinates on the specified axis and can be used with either axis at will.
Inspired by this example I'm trying to write a little matplotlib program that allows the user to drag and drop datapoints in a scatter plot dynamically. In contrast to the example which uses a bar plot (and thus allows dragging of rectangles) my goal was to achieve the same with other patches, like for instance a circle (any patch that is more scatter-plot-compatible than a rectangle would do). However I'm stuck at the point of updating the position of my patch. While a Rectangle provides a function set_xy I cannot find a direct analog for Cirlce or Ellipse. Obtaining the position of a circle is also less straightforward that for a rectangle, but is possible via obtaining the bounding box. The missing piece now is to find a way to update the position of my patch. Any hint on how to achieve this would be great! The current minimal working example would look like this:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
class DraggablePatch:
def __init__(self, patch):
self.patch = patch
self.storedPosition = None
self.connect()
def getPosOfPatch(self, marker):
ext = marker.get_extents().get_points()
x0 = ext[0,0]
y0 = ext[0,1]
x1 = ext[1,0]
y1 = ext[1,1]
return 0.5*(x0+x1), 0.5*(y0+y1)
def connect(self):
'connect to all the events we need'
self.cidpress = self.patch.figure.canvas.mpl_connect('button_press_event', self.onPress)
self.cidmotion = self.patch.figure.canvas.mpl_connect('motion_notify_event', self.onMove)
def onPress(self, event):
'on button press we will see if the mouse is over us and store some data'
contains, attrd = self.patch.contains(event)
if contains:
self.storedPosition = self.getPosOfPatch(self.patch), event.xdata, event.ydata
def onMove(self, event):
'how to update an circle?!'
contains, attrd = self.patch.contains(event)
if contains and self.storedPosition is not None:
oldPos, oldEventXData, oldEventYData = self.storedPosition
dx = event.xdata - oldEventXData
dy = event.ydata - oldEventYData
newX = oldPos[0] + dx
newY = oldPos[1] + dy
print "now I would like to move my patch to", newX, newY
def myPatch(x,y):
return patches.Circle((x,y), radius=.05, alpha=0.5)
N = 10
x = np.random.random(N)
y = np.random.random(N)
patches = [myPatch(x[i], y[i]) for i in range(N)]
fig = plt.figure()
ax = fig.add_subplot(111)
drs = []
for patch in patches:
ax.add_patch(patch)
dr = DraggablePatch(patch)
drs.append(dr)
plt.show()
It's a bit annoying that it's inconsistent, but to update the position of a circle, set circ.center = new_x, new_y.
As a simple (non-draggable) example:
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
class InteractiveCircle(object):
def __init__(self):
self.fig, self.ax = plt.subplots()
self.ax.axis('equal')
self.circ = Circle((0.5, 0.5), 0.1)
self.ax.add_artist(self.circ)
self.ax.set_title('Click to move the circle')
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
def on_click(self, event):
if event.inaxes is None:
return
self.circ.center = event.xdata, event.ydata
self.fig.canvas.draw()
def show(self):
plt.show()
InteractiveCircle().show()