matplotlib: update position of patches (or: set_xy for circles) - python

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()

Related

Drag a "bar" through a graph and present values in a textbox? [duplicate]

I'm trying to add vertical lines to a matplotlib plot dynmically when a user clicks on a particular point.
import matplotlib.pyplot as plt
import matplotlib.dates as mdate
class PointPicker(object):
def __init__(self,dates,values):
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111)
self.lines2d, = self.ax.plot_date(dates, values, linestyle='-',picker=5)
self.fig.canvas.mpl_connect('pick_event', self.onpick)
self.fig.canvas.mpl_connect('key_press_event', self.onpress)
def onpress(self, event):
"""define some key press events"""
if event.key.lower() == 'q':
sys.exit()
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
print self.ax.axvline(x=x, visible=True)
x = mdate.num2date(x)
print x,y,type(x)
if __name__ == '__main__':
import numpy as np
import datetime
dates=[datetime.datetime.now()+i*datetime.timedelta(days=1) for i in range(100)]
values = np.random.random(100)
plt.ion()
p = PointPicker(dates,values)
plt.show()
Here's an (almost) working example. When I click a point, the onpick method is indeed called and the data seems to be correct, but no vertical line shows up. What do I need to do to get the vertical line to show up?
Thanks
You need to update the canvas drawing (self.fig.canvas.draw()):
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
L = self.ax.axvline(x=x)
self.fig.canvas.draw()

How can I add functionality to a user defined button on matplotlib toolbar?

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 :-)

Is it possible to select data in a plot with python and remove data selected?

Hello i did a python file which allows to select datas thanks to creating a polygon and then i would like to delete the points inside the polygon. This is my code :
import pylab as plt
import numpy as np
from shapely.geometry import Point
from shapely.geometry.polygon import Polygon
#from matplotlib.widgets import Button
a = np.array([1,2,3,5,7])
b = np.array([1,6,5,7,9])
plt.plot(a,b, '.')
myList=[]
class Canvas(object):
def __init__(self,ax):
self.ax = ax
# Create handle for a path of connected points
self.path, = ax.plot([],[],'o-',lw=3)
self.vert = []
self.ax.set_title('LEFT: new point, MIDDLE: delete last point, RIGHT: close polygon')
self.x = []
self.y = []
self.mouse_button = {1: self._add_point, 2: self._delete_point, 3: self._close_polygon}
def set_location(self,event):
if event.inaxes:
self.x = event.xdata
self.y = event.ydata
def _add_point(self):
self.vert.append((self.x,self.y))
def _delete_point(self):
if len(self.vert)>0:
self.vert.pop()
def _close_polygon(self):
self.vert.append(self.vert[0])
for i in range(0,a.size):
myList.append(Point(a[i],b[i]))
polygon = Polygon(self.vert)
for i1 in range(0,a.size):
if polygon.contains(myList[i1]):
global a,b
a = np.delete(a,i1)
b = np.delete(b,i1)
plt.plot(a,b, 'g.')
def update_path(self,event):
# If the mouse pointer is not on the canvas, ignore buttons
if not event.inaxes: return
# Do whichever action correspond to the mouse button clicked
self.mouse_button[event.button]()
x = [self.vert[k][0] for k in range(len(self.vert))]
y = [self.vert[k][1] for k in range(len(self.vert))]
self.path.set_data(x,y)
plt.draw()
if __name__ == '__main__':
fig = plt.figure(1,(8,8))
ax = fig.add_subplot(111)
cnv = Canvas(ax)
plt.connect('button_press_event',cnv.update_path)
plt.connect('motion_notify_event',cnv.set_location)
class Index(object):
ind = 0
def next(self, event):
plt.draw()
def prev(self, event):
plt.draw()
callback = Index()
plt.show()
But i have two problems : firstly i don't know why but the points inside the polygon stay in the figure. And then, the points inside the polygon become green whereas they are not outside the polygon. Thank you very much for your help !

Python: Return coordinate info on mouse click

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.

cursor tracking using matplotlib and twinx

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.

Categories

Resources