I recently asked a question about creating a draggable scatter and with the help of someone I was able to come up with a working example. See 'PathCollection' not iterable - creating a draggable scatter plot.
I'm now trying to use the DraggableScatter class I created with an animated plot using blitting.
I've tried to attach the DraggableScatter class in multiple places, e.g., after initializing the scatter, in the init function and in the update function. In the first cases, the DraggableScatter's scatter is empty, which makes sense but obviously doesn't work. In the other two, the clicks seem not to be captured.
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
class DraggableScatter():
epsilon = 5
def __init__(self, scatter):
self.scatter = scatter
self._ind = None
self.ax = scatter.axes
self.canvas = self.ax.figure.canvas
self.canvas.mpl_connect('button_press_event', self.button_press_callback)
self.canvas.mpl_connect('button_release_event', self.button_release_callback)
self.canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
def get_ind_under_point(self, event):
xy = np.asarray(self.scatter.get_offsets())
xyt = self.ax.transData.transform(xy)
xt, yt = xyt[:, 0], xyt[:, 1]
d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2)
ind = d.argmin()
if d[ind] >= self.epsilon:
ind = None
return ind
def button_press_callback(self, event):
if event.inaxes is None:
return
if event.button != 1:
return
self._ind = self.get_ind_under_point(event)
def button_release_callback(self, event):
if event.button != 1:
return
self._ind = None
def motion_notify_callback(self, event):
if self._ind is None:
return
if event.inaxes is None:
return
if event.button != 1:
return
x, y = event.xdata, event.ydata
xy = np.asarray(self.scatter.get_offsets())
xy[self._ind] = np.array([x, y])
self.scatter.set_offsets(xy)
self.canvas.draw_idle()
fig, ax = plt.subplots(1, 1)
scatter = ax.scatter([],[])
def init():
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
return scatter,
def update(frame):
scatter = ax.scatter(np.random.rand(10), np.random.rand(10), marker ='o')
ds = DraggableScatter(scatter)
return scatter,
ani = FuncAnimation(fig=fig, func=update, init_func=init, blit=True, interval=5000)
plt.show()
What is the right way to do this?
This works with the GTK3Cairo backend. (It does not work using TkAgg, Qt4Agg, Qt5Agg, GTK3Agg.)
Create the DraggableScatter once, then use ds.scatter.set_offsets to change the scatter point data inside the update function:
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
class DraggableScatter():
epsilon = 5
def __init__(self, scatter):
self.scatter = scatter
self._ind = None
self.ax = scatter.axes
self.canvas = self.ax.figure.canvas
self.canvas.mpl_connect('button_press_event', self.button_press_callback)
self.canvas.mpl_connect('button_release_event', self.button_release_callback)
self.canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
def get_ind_under_point(self, event):
xy = np.asarray(self.scatter.get_offsets())
xyt = self.ax.transData.transform(xy)
xt, yt = xyt[:, 0], xyt[:, 1]
d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2)
ind = d.argmin()
if d[ind] >= self.epsilon:
ind = None
return ind
def button_press_callback(self, event):
if event.inaxes is None:
return
if event.button != 1:
return
self._ind = self.get_ind_under_point(event)
def button_release_callback(self, event):
if event.button != 1:
return
self._ind = None
def motion_notify_callback(self, event):
if self._ind is None:
return
if event.inaxes is None:
return
if event.button != 1:
return
x, y = event.xdata, event.ydata
xy = np.asarray(self.scatter.get_offsets())
xy[self._ind] = np.array([x, y])
self.scatter.set_offsets(xy)
self.canvas.draw_idle()
fig, ax = plt.subplots(1, 1)
scatter = ax.scatter(np.random.rand(10), np.random.rand(10), marker ='o')
ds = DraggableScatter(scatter)
def init():
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
return ds.scatter,
def update(frame, ds):
x, y = np.random.rand(10), np.random.rand(10)
ds.scatter.set_offsets(np.column_stack([x, y]))
return ds.scatter,
ani = FuncAnimation(fig=fig, func=update, init_func=init, fargs=[ds], blit=True,
interval=5000)
plt.show()
Related
I'm doing a Python program to fit Bezier curves to the eye contour (for example one curve that is adapted to the bottom line of the brow and the other to the lid's contour ). In order to achieve this, I'm trying to put the image of the eye as the background of a plot, and then interactive draw a line with the mousse that fits the best. My problem is that I need an arc and not a circle as I have done but I don't know how to solve this.
My initial image and the circle I have to fit but I only want an arc. The line fits the brow after using my mouse (the pink part is what I want). It's important to say I have to convert this arc or line into a Bézier curve, I have a program that does this function but It will be better If I could directly draw a Bezier line in the image but I don't know-how so the main problem is to have an arc or line but nos a circle because I need an open curve. I have to use the Bezier lines because I have to calculate some medical parameters with them, so I need their coordinates.
import numpy as np
import matplotlib
import shapely
import matplotlib.pyplot as plt
from scipy.interpolate import interp1d
from matplotlib.lines import Line2D
from matplotlib.artist import Artist
from matplotlib.bezier import find_control_points
class PolygonInteractor(object):
"""
A polygon editor.
https://matplotlib.org/gallery/event_handling/poly_editor.html
Key-bindings
't' toggle vertex markers on and off. When vertex markers are on,
you can move them, delete them
'd' delete the vertex under point
'i' insert a vertex at point. You must be within epsilon of the
line connecting two existing vertices
"""
showverts = True
epsilon = 5 # max pixel distance to count as a vertex hit
def __init__(self, ax, poly, visible=False):
if poly.figure is None:
raise RuntimeError('You must first add the polygon to a figure '
'or canvas before defining the interactor')
self.ax = ax
canvas = poly.figure.canvas
self.poly = poly
self.poly.set_visible(visible)
x, y = zip(*self.poly.xy)
self.line = Line2D(x, y, ls="",
marker='*', linewidth=1, markerfacecolor='green',
animated=True)
self.ax.add_line(self.line)
self.cid = self.poly.add_callback(self.poly_changed)
self._ind = None # the active vert
canvas.mpl_connect('draw_event', self.draw_callback)
canvas.mpl_connect('button_press_event', self.button_press_callback)
canvas.mpl_connect('key_press_event', self.key_press_callback)
canvas.mpl_connect('button_release_event', self.button_release_callback)
canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
self.canvas = canvas
x,y = self.interpolate()
self.line2 = Line2D(x, y, animated=True)
self.ax.add_line(self.line2)
def interpolate(self):
x, y = self.poly.xy[:].T
i = np.arange(len(x))
interp_i = np.linspace(0, i.max(), 100 * i.max())
xi = interp1d(i, x, kind='cubic')(interp_i)
yi = interp1d(i, y, kind='cubic')(interp_i)
return xi,yi
def draw_callback(self, event):
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.draw_artist(self.poly)
self.ax.draw_artist(self.line)
self.ax.draw_artist(self.line2)
# do not need to blit here, this will fire before the screen is
# updated
def poly_changed(self, poly):
'this method is called whenever the polygon object is called'
# only copy the artist props to the line (except visibility)
vis = self.line.get_visible()
Artist.update_from(self.line, poly)
self.line.set_visible(vis) # don't use the poly visibility state
def get_ind_under_point(self, event):
'get the index of the vertex under point if within epsilon tolerance'
# display coords
xy = np.asarray(self.poly.xy)
xyt = self.poly.get_transform().transform(xy)
xt, yt = xyt[:, 0], xyt[:, 1]
d = np.hypot(xt - event.x, yt - event.y)
indseq, = np.nonzero(d == d.min())
ind = indseq[0]
if d[ind] >= self.epsilon:
ind = None
return ind
def button_press_callback(self, event):
'whenever a mouse button is pressed'
if not self.showverts:
return
if event.inaxes is None:
return
if event.button != 1:
return
self._ind = self.get_ind_under_point(event)
def button_release_callback(self, event):
'whenever a mouse button is released'
if not self.showverts:
return
if event.button != 1:
return
self._ind = None
def key_press_callback(self, event):
'whenever a key is pressed'
if not event.inaxes:
return
if event.key == 't':
self.showverts = not self.showverts
self.line.set_visible(self.showverts)
if not self.showverts:
self._ind = None
elif event.key == 'd':
ind = self.get_ind_under_point(event)
if ind is not None:
self.poly.xy = np.delete(self.poly.xy,
ind, axis=0)
self.line.set_data(zip(*self.poly.xy))
elif event.key == 'i':
xys = self.poly.get_transform().transform(self.poly.xy)
p = event.x, event.y # display coords
for i in range(len(xys) - 1):
s0 = xys[i]
s1 = xys[i + 1]
d = dist_point_to_segment(p, s0, s1)
if d <= self.epsilon:
self.poly.xy = np.insert(
self.poly.xy, i+1,
[event.xdata, event.ydata],
axis=0)
self.line.set_data(zip(*self.poly.xy))
break
if self.line.stale:
self.canvas.draw_idle()
def motion_notify_callback(self, event):
'on mouse movement'
if not self.showverts:
return
if self._ind is None:
return
if event.inaxes is None:
return
if event.button != 1:
return
x, y = event.xdata, event.ydata
self.poly.xy[self._ind] = x, y
if self._ind == 0:
self.poly.xy[-1] = x, y
elif self._ind == len(self.poly.xy) - 1:
self.poly.xy[0] = x, y
self.line.set_data(zip(*self.poly.xy))
x,y = self.interpolate()
self.line2.set_data(x,y)
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.poly)
self.ax.draw_artist(self.line)
self.ax.draw_artist(self.line2)
self.canvas.blit(self.ax.bbox)
if __name__ == '__main__':
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
theta = np.arange(0, 2*np.pi, 0.1)
r = 1.5
xs = r*np.cos(theta)
ys = r*np.sin(theta)
xs = (921, 951, 993, 1000)
ys = (1181, 1230, 1243, 257)
poly = Polygon(list(zip(xs, ys)), animated=True)
#poly = LineString([(0, 0), (1, 1)])
img = plt.imread("/Users/raquel/Desktop/TFG/IMÁGENES/Unknown.jpeg")
fig, ax = plt.subplots()
img = ax.imshow(img, extent=[0, 1300, 0, 1300])
ax.add_patch(poly)
p = PolygonInteractor(ax, poly, visible=False)
ax.set_title('Click and drag a point to move it')
x = ax.set_xlim((0, 1300))
y = ax.set_ylim((0, 1300))
plt.show()
I have the following code (based on Matplotlib plot zooming with scroll wheel). I have tried to embed it into a PySimpleGUI window tab, so far I have not been able to do so.
I managed to make it work with a Tkinter window, but my main GUI is mainly PySImpleGUI.
from matplotlib.pyplot import figure, show
import numpy import PySimpleGUI as sg
import matplotlib.pyplot as plt
class ZoomPan:
def __init__(self):
self.press = None
self.cur_xlim = None
self.cur_ylim = None
self.x0 = None
self.y0 = None
self.x1 = None
self.y1 = None
self.xpress = None
self.ypress = None
def zoom_factory(self, ax, base_scale = 2.):
def zoom(event):
cur_xlim = ax.get_xlim()
cur_ylim = ax.get_ylim()
xdata = event.xdata # get event x location
ydata = event.ydata # get event y location
if event.button == 'up':
# deal with zoom in
scale_factor = 1 / base_scale
elif event.button == 'down':
# deal with zoom out
scale_factor = base_scale
else:
# deal with something that should never happen
scale_factor = 1
print (event.button)
new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor
new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor
relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0])
rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0])
ax.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)])
ax.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)])
ax.figure.canvas.draw()
fig = ax.get_figure() # get the figure of interest
fig.canvas.mpl_connect('scroll_event', zoom)
return zoom
def pan_factory(self, ax):
def onPress(event):
if event.inaxes != ax: return
self.cur_xlim = ax.get_xlim()
self.cur_ylim = ax.get_ylim()
self.press = self.x0, self.y0, event.xdata, event.ydata
self.x0, self.y0, self.xpress, self.ypress = self.press
def onRelease(event):
self.press = None
ax.figure.canvas.draw()
def onMotion(event):
if self.press is None: return
if event.inaxes != ax: return
dx = event.xdata - self.xpress
dy = event.ydata - self.ypress
self.cur_xlim -= dx
self.cur_ylim -= dy
ax.set_xlim(self.cur_xlim)
ax.set_ylim(self.cur_ylim)
ax.figure.canvas.draw()
fig = ax.get_figure() # get the figure of interest
# attach the call back
fig.canvas.mpl_connect('button_press_event',onPress)
fig.canvas.mpl_connect('button_release_event',onRelease)
fig.canvas.mpl_connect('motion_notify_event',onMotion)
#return the function
return onMotion
"FIGURE"
plt.figure(1)
fig = plt.gcf()
ax = fig.add_subplot(111)
ax.clear()
ax.set_title('')
"RANDOM PLOT"
x,y,s,c = numpy.random.rand(4,200) s *= 200
ax.scatter(x,y,s,c, label='label')
"FIGURE CONSTANT"
ax.grid()
ax.set_xlabel('COORDENADA ESTE', size=15)
ax.set_ylabel('COORDENADA NORTE', size=15)
plt.tight_layout(pad=0.05, h_pad=0.05, w_pad=0.05, rect=(0.05,-0.05,0.95,0.95))
ax.legend(loc=2, bbox_to_anchor=(-0.15, 1))
mng = plt.get_current_fig_manager()
mng.window.showMaximized()
"PLOT"
zp = ZoomPan()
figZoom = zp.zoom_factory(ax, base_scale = 1.1)
figPan= zp.pan_factory(ax)
show()
You began your plot as you would normally do, then you have to create the actual fig for the pysimplegui window canvas as follows:
def draw_figure(canvas, figure, loc=(0, 0)):
figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
figure_canvas_agg.draw()
figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
return figure_canvas_agg
such as:
fig_canvas_agg = draw_figure(window['canvas'].TKCanvas, fig)
with fig= plt.figure() and window['canvas']=sg.Canvas(key='canvas') on the window layout for PYSImpleGUI, and then you apply the ZoomPan class as follows:
zp = plot_func()
figZoom = zp.zoom_factory(ax1, base_scale=1.7)
figPan = zp.pan_factory(ax1)
I have a class which allows the user to create a region using the mouse. Now I want to create multiple regions by appending objects to a list.
However, when I try to append it just runs once and I get 2 copies of the same region. It seems that both are running in parallel at the same time. Is there an easy workaround for this problem?
import numpy as np
import sys
import matplotlib.pyplot as plt
import matplotlib.path as mplPath
import matplotlib.image as mpimg
class create_region():
def __init__(self, img):
if isinstance(img, np.ndarray):
plt.imshow(img)
fig = plt.gcf()
fig.set_size_inches(12,9)
else:
print('Error: please include an image np.ndarray, create_region(img)')
return
self.img = img
self.img_size = np.shape(img)
self.line = None
self.roicolor = (.415,1,.302) # Use neon green to stand out
self.markersize = 4
self.x_pts = []
self.y_pts = []
self.previous_pt = []
self.start_point = []
self.end_point = []
self.fig = fig
self.ax = plt.gca()
self.__ID1 = self.fig.canvas.mpl_connect(
'motion_notify_event', self.__motion_notify_callback)
self.__ID2 = self.fig.canvas.mpl_connect(
'button_press_event', self.__button_press_callback)
if sys.flags.interactive:
plt.show(block=False)
else:
plt.show()
def __motion_notify_callback(self, event):
if event.inaxes:
#ax = event.inaxes
x, y = event.xdata, event.ydata
# Move line around
if (event.button == None or event.button == 1) and self.line != None:
self.line.set_data([self.previous_pt[0], x],
[self.previous_pt[1], y])
self.fig.canvas.draw()
def __button_press_callback(self, event):
if event.inaxes:
x, y = event.xdata, event.ydata
ax = event.inaxes
# If you press the left button, single click
if event.button == 1 and event.dblclick == False:
if self.line == None: # if there is no line, create a line
self.line = plt.Line2D([x, x],
[y, y],
marker='o',
color=self.roicolor,
markersize = self.markersize)
self.start_point = [x,y]
self.previous_pt = self.start_point
self.x_pts=[x]
self.y_pts=[y]
ax.add_line(self.line)
self.fig.canvas.draw()
# add a segment
else: # if there is a line, create a segment
self.line = plt.Line2D([self.previous_pt[0], x],
[self.previous_pt[1], y],
marker = 'o',color=self.roicolor,
markersize = self.markersize)
self.previous_pt = [x,y]
self.x_pts.append(x)
self.y_pts.append(y)
event.inaxes.add_line(self.line)
self.fig.canvas.draw()
# close the loop and disconnect
elif ((event.button == 1 and event.dblclick==True) or
(event.button == 3 and event.dblclick==False)) and self.line != None:
self.fig.canvas.mpl_disconnect(self.__ID1) #joerg
self.fig.canvas.mpl_disconnect(self.__ID2) #joerg
self.line.set_data([self.previous_pt[0],
self.start_point[0]],
[self.previous_pt[1],
self.start_point[1]])
ax.add_line(self.line)
self.fig.canvas.draw()
self.line = None
if sys.flags.interactive:
pass
else:
#figure has to be closed so that code can continue
plt.close(self.fig)
# create zeros image
img=np.zeros((768,1024),dtype='uint8')
# try to append 2 different user regions to list
# seems both run simultaneously, returns 2 copies of 1 region
M = []
for i in range(2):
M.append(create_region(img))
I'm trying to make an interactive plot using matplotlib that creates a line segment with two handles at the endpoints. You can click and drag the handles and the line will refresh to match the positions specified in this way, in a similar fashion to this matplotlib example poly_editor: (if you see the example, imagine that I want the same thing but with just one edge of the polygon).
I have tried altering the poly_editor code to work with just the Line2D element, and my program runs without any errors, except that it doesn't draw anything on the axis at all. I think it might be an error in the scope of the variables or something to do with the draw calls from matplotlib. Any guidance as to what the errors are would be greatly appreciated.
Edit: I advanced some more, simplified the code and now I can get it to draw the line and print the index of the nearest vertex within epsilon distance, but the line stays stationary and does not animate. The updated code is bellow
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.lines import Line2D
class LineBuilder(object):
epsilon = 0.5
def __init__(self, line):
canvas = line.figure.canvas
self.canvas = canvas
self.line = line
self.axes = line.axes
self.xs = list(line.get_xdata())
self.ys = list(line.get_ydata())
self.ind = None
canvas.mpl_connect('button_press_event', self.button_press_callback)
canvas.mpl_connect('button_release_event', self.button_release_callback)
canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
def get_ind(self, event):
x = np.array(self.line.get_xdata())
y = np.array(self.line.get_ydata())
d = np.sqrt((x-event.xdata)**2 + (y - event.ydata)**2)
if min(d) > self.epsilon:
return None
if d[0] < d[1]:
return 0
else:
return 1
def button_press_callback(self, event):
if event.button != 1:
return
self.ind = self.get_ind(event)
print(self.ind)
self.line.set_animated(True)
self.canvas.draw()
self.background = self.canvas.copy_from_bbox(self.line.axes.bbox)
self.axes.draw_artist(self.line)
self.canvas.blit(self.axes.bbox)
def button_release_callback(self, event):
if event.button != 1:
return
self.ind = None
self.line.set_animated(False)
self.background = None
self.line.figure.canvas.draw()
def motion_notify_callback(self, event):
if event.inaxes != self.line.axes:
return
if event.button != 1:
return
if self.ind is None:
return
self.xs[self.ind] = event.xdata
self.ys[self.ind] = event.ydata
self.line.set_data(self.xs, self.ys)
self.canvas.restore_region(self.background)
self.axes.draw_artist(self.line)
self.canvas.blit(self.axes.bbox)
if __name__ == '__main__':
fig, ax = plt.subplots()
line = Line2D([0,1], [0,1], marker='o', markerfacecolor='red')
ax.add_line(line)
linebuilder = LineBuilder(line)
ax.set_title('click to create lines')
ax.set_xlim(-2,2)
ax.set_ylim(-2,2)
plt.show()
Thanks in advance, Kevin.
Okay, I solved the problem. The new code (above) actually works, there was a mistake in it. The mpl_connect call for the motion notify event had the wrong event type, now it is working as intended.
I am new here so hope I don't make to many mistakes by replying to this self.replied question. :)
First thanks for posting this, it helped me a lot by saving some time, I wanted almost exactly this code. I did some updates that I propose here, so that it's possible to manipulate more than two points, and use the key handling events to create or delete points in the line, as the PolygonInteractor does.
from matplotlib.lines import Line2D
import matplotlib.pyplot as plt
import numpy as np
def dist(x, y):
"""
Return the distance between two points.
"""
d = x - y
return np.sqrt(np.dot(d, d))
def dist_point_to_segment(p, s0, s1):
"""
Get the distance of a point to a segment.
*p*, *s0*, *s1* are *xy* sequences
This algorithm from
http://geomalgorithms.com/a02-_lines.html
"""
v = s1 - s0
w = p - s0
c1 = np.dot(w, v)
if c1 <= 0:
return dist(p, s0)
c2 = np.dot(v, v)
if c2 <= c1:
return dist(p, s1)
b = c1 / c2
pb = s0 + b * v
return dist(p, pb)
class LineBuilder(object):
epsilon = 30 #in pixels
def __init__(self, line):
canvas = line.figure.canvas
self.canvas = canvas
self.line = line
self.axes = line.axes
self.xs = list(line.get_xdata())
self.ys = list(line.get_ydata())
self.ind = None
canvas.mpl_connect('button_press_event', self.button_press_callback)
canvas.mpl_connect('button_release_event', self.button_release_callback)
canvas.mpl_connect('key_press_event', self.key_press_callback)
canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
def get_ind(self, event):
xy = np.asarray(self.line._xy)
xyt = self.line.get_transform().transform(xy)
x, y = xyt[:, 0], xyt[:, 1]
d = np.sqrt((x-event.x)**2 + (y - event.y)**2)
indseq, = np.nonzero(d == d.min())
ind = indseq[0]
if d[ind] >= self.epsilon:
ind = None
return ind
def button_press_callback(self, event):
if event.button != 1:
return
if event.inaxes is None:
return
self.ind = self.get_ind(event)
print(self.ind)
self.line.set_animated(True)
self.canvas.draw()
self.background = self.canvas.copy_from_bbox(self.line.axes.bbox)
self.axes.draw_artist(self.line)
self.canvas.blit(self.axes.bbox)
def button_release_callback(self, event):
if event.button != 1:
return
self.ind = None
self.line.set_animated(False)
self.background = None
self.line.figure.canvas.draw()
def motion_notify_callback(self, event):
if event.inaxes != self.line.axes:
return
if event.button != 1:
return
if self.ind is None:
return
self.xs[self.ind] = event.xdata
self.ys[self.ind] = event.ydata
self.line.set_data(self.xs, self.ys)
self.canvas.restore_region(self.background)
self.axes.draw_artist(self.line)
self.canvas.blit(self.axes.bbox)
def key_press_callback(self, event):
"""Callback for key presses."""
if not event.inaxes:
return
elif event.key == 'd':
ind = self.get_ind(event)
if ind is not None and len(self.xs) > 2:
self.xs = np.delete(self.xs, ind)
self.ys = np.delete(self.ys, ind)
self.line.set_data(self.xs, self.ys)
self.axes.draw_artist(self.line)
self.canvas.draw_idle()
elif event.key == 'i':
p = np.array([event.x, event.y]) # display coords
xy = np.asarray(self.line._xy)
xyt = self.line.get_transform().transform(xy)
for i in range(len(xyt) - 1):
s0 = xyt[i]
s1 = xyt[i+1]
d = dist_point_to_segment(p, s0, s1)
if d <= self.epsilon:
self.xs = np.insert(self.xs, i+1, event.xdata)
self.ys = np.insert(self.ys, i+1, event.ydata)
self.line.set_data(self.xs, self.ys)
self.axes.draw_artist(self.line)
self.canvas.draw_idle()
break
if __name__ == '__main__':
fig, ax = plt.subplots()
line = Line2D([0,0.5,1], [0,0.5,1], marker = 'o', markerfacecolor = 'red')
ax.add_line(line)
linebuilder = LineBuilder(line)
ax.set_title('click to create lines')
ax.set_xlim(-2,2)
ax.set_ylim(-2,2)
plt.show()
This actually leads to a question/problem i have but it will be in another message.
Kristen
I need to have 2 draggable points in a figureCanvas. But I have a supplementary constraint: the 2 points must be linked by a line.
When I drag a point, of course (it wouldn't be funny otherwise), the line must be dynamically drawn, and still linked to the 2 points.
I managed to create the 2 draggable points, with this topic:
Matplotlib drag overlapping points interactively
I modified a bit the code to use it trough a subclass of FigureCanvas (to later include the graph in a PyQt application):
import matplotlib.pyplot as plt
import matplotlib.patches as patches
class DraggablePoint:
# https://stackoverflow.com/questions/21654008/matplotlib-drag-overlapping-points-interactively
lock = None # only one can be animated at a time
def __init__(self, parent, x=0.1, y=0.1):
self.parent = parent
self.point = patches.Ellipse((x, y), 0.01, 0.03, fc='r', alpha=0.5)
self.x = x
self.y = y
parent.fig.axes[0].add_patch(self.point)
self.press = None
self.background = None
self.connect()
def connect(self):
'connect to all the events we need'
self.cidpress = self.point.figure.canvas.mpl_connect('button_press_event', self.on_press)
self.cidrelease = self.point.figure.canvas.mpl_connect('button_release_event', self.on_release)
self.cidmotion = self.point.figure.canvas.mpl_connect('motion_notify_event', self.on_motion)
def on_press(self, event):
if event.inaxes != self.point.axes: return
if DraggablePoint.lock is not None: return
contains, attrd = self.point.contains(event)
if not contains: return
self.press = (self.point.center), event.xdata, event.ydata
DraggablePoint.lock = self
# draw everything but the selected rectangle and store the pixel buffer
canvas = self.point.figure.canvas
axes = self.point.axes
self.point.set_animated(True)
canvas.draw()
self.background = canvas.copy_from_bbox(self.point.axes.bbox)
# now redraw just the rectangle
axes.draw_artist(self.point)
# and blit just the redrawn area
canvas.blit(axes.bbox)
def on_motion(self, event):
if DraggablePoint.lock is not self:
return
if event.inaxes != self.point.axes: return
self.point.center, xpress, ypress = self.press
dx = event.xdata - xpress
dy = event.ydata - ypress
self.point.center = (self.point.center[0]+dx, self.point.center[1]+dy)
canvas = self.point.figure.canvas
axes = self.point.axes
# restore the background region
canvas.restore_region(self.background)
# redraw just the current rectangle
axes.draw_artist(self.point)
# blit just the redrawn area
canvas.blit(axes.bbox)
def on_release(self, event):
'on release we reset the press data'
if DraggablePoint.lock is not self:
return
self.press = None
DraggablePoint.lock = None
# turn off the rect animation property and reset the background
self.point.set_animated(False)
self.background = None
# redraw the full figure
self.point.figure.canvas.draw()
self.x = self.point.center[0]
self.y = self.point.center[1]
def disconnect(self):
'disconnect all the stored connection ids'
self.point.figure.canvas.mpl_disconnect(self.cidpress)
self.point.figure.canvas.mpl_disconnect(self.cidrelease)
self.point.figure.canvas.mpl_disconnect(self.cidmotion)
There will only be 2 points on the future graph, and I can access the other point from the class DraggablePoint trough self.parent.
I think I need to draw a line between the 2 points, in the function on_motion. But I tried and found nothing.
Do you have an idea about how to achieve that ?
Ok I finally found the solution. I post it here for those who might need it. This code basically allow to have 2 draggable points linked by a line. If you move one of the points, the line follows. Very useful to make a baseline in scientific applications.
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.lines import Line2D
class DraggablePoint:
# http://stackoverflow.com/questions/21654008/matplotlib-drag-overlapping-points-interactively
lock = None # only one can be animated at a time
def __init__(self, parent, x=0.1, y=0.1, size=0.1):
self.parent = parent
self.point = patches.Ellipse((x, y), size, size * 3, fc='r', alpha=0.5, edgecolor='r')
self.x = x
self.y = y
parent.fig.axes[0].add_patch(self.point)
self.press = None
self.background = None
self.connect()
if self.parent.list_points:
line_x = [self.parent.list_points[0].x, self.x]
line_y = [self.parent.list_points[0].y, self.y]
self.line = Line2D(line_x, line_y, color='r', alpha=0.5)
parent.fig.axes[0].add_line(self.line)
def connect(self):
'connect to all the events we need'
self.cidpress = self.point.figure.canvas.mpl_connect('button_press_event', self.on_press)
self.cidrelease = self.point.figure.canvas.mpl_connect('button_release_event', self.on_release)
self.cidmotion = self.point.figure.canvas.mpl_connect('motion_notify_event', self.on_motion)
def on_press(self, event):
if event.inaxes != self.point.axes: return
if DraggablePoint.lock is not None: return
contains, attrd = self.point.contains(event)
if not contains: return
self.press = (self.point.center), event.xdata, event.ydata
DraggablePoint.lock = self
# draw everything but the selected rectangle and store the pixel buffer
canvas = self.point.figure.canvas
axes = self.point.axes
self.point.set_animated(True)
if self == self.parent.list_points[1]:
self.line.set_animated(True)
else:
self.parent.list_points[1].line.set_animated(True)
canvas.draw()
self.background = canvas.copy_from_bbox(self.point.axes.bbox)
# now redraw just the rectangle
axes.draw_artist(self.point)
# and blit just the redrawn area
canvas.blit(axes.bbox)
def on_motion(self, event):
if DraggablePoint.lock is not self:
return
if event.inaxes != self.point.axes: return
self.point.center, xpress, ypress = self.press
dx = event.xdata - xpress
dy = event.ydata - ypress
self.point.center = (self.point.center[0]+dx, self.point.center[1]+dy)
canvas = self.point.figure.canvas
axes = self.point.axes
# restore the background region
canvas.restore_region(self.background)
# redraw just the current rectangle
axes.draw_artist(self.point)
if self == self.parent.list_points[1]:
axes.draw_artist(self.line)
else:
self.parent.list_points[1].line.set_animated(True)
axes.draw_artist(self.parent.list_points[1].line)
self.x = self.point.center[0]
self.y = self.point.center[1]
if self == self.parent.list_points[1]:
line_x = [self.parent.list_points[0].x, self.x]
line_y = [self.parent.list_points[0].y, self.y]
self.line.set_data(line_x, line_y)
else:
line_x = [self.x, self.parent.list_points[1].x]
line_y = [self.y, self.parent.list_points[1].y]
self.parent.list_points[1].line.set_data(line_x, line_y)
# blit just the redrawn area
canvas.blit(axes.bbox)
def on_release(self, event):
'on release we reset the press data'
if DraggablePoint.lock is not self:
return
self.press = None
DraggablePoint.lock = None
# turn off the rect animation property and reset the background
self.point.set_animated(False)
if self == self.parent.list_points[1]:
self.line.set_animated(False)
else:
self.parent.list_points[1].line.set_animated(False)
self.background = None
# redraw the full figure
self.point.figure.canvas.draw()
self.x = self.point.center[0]
self.y = self.point.center[1]
def disconnect(self):
'disconnect all the stored connection ids'
self.point.figure.canvas.mpl_disconnect(self.cidpress)
self.point.figure.canvas.mpl_disconnect(self.cidrelease)
self.point.figure.canvas.mpl_disconnect(self.cidmotion)
UPDATE:
How to use the DraggablePoint class, with PyQt5:
#!/usr/bin/python
# -*-coding:Utf-8 -*
import sys
import matplotlib
matplotlib.use("Qt5Agg")
from PyQt5 import QtWidgets, QtGui
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
# Personnal modules
from drag import DraggablePoint
class MyGraph(FigureCanvas):
"""A canvas that updates itself every second with a new plot."""
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = self.fig.add_subplot(111)
self.axes.grid(True)
FigureCanvas.__init__(self, self.fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
# To store the 2 draggable points
self.list_points = []
self.show()
self.plotDraggablePoints([0.1, 0.1], [0.2, 0.2], [0.1, 0.1])
def plotDraggablePoints(self, xy1, xy2, size=None):
"""Plot and define the 2 draggable points of the baseline"""
# del(self.list_points[:])
self.list_points.append(DraggablePoint(self, xy1[0], xy1[1], size))
self.list_points.append(DraggablePoint(self, xy2[0], xy2[1], size))
self.updateFigure()
def clearFigure(self):
"""Clear the graph"""
self.axes.clear()
self.axes.grid(True)
del(self.list_points[:])
self.updateFigure()
def updateFigure(self):
"""Update the graph. Necessary, to call after each plot"""
self.draw()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = MyGraph()
sys.exit(app.exec_())
Here is my simple solution with the additionnal feature to add or remove points.
You have then a draggable segmented line with controls on points.
The code is simple despite events handling. Improvements are welcome.
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.lines import Line2D
#------------------------------------------------
listLabelPoints = []
point_alpha_default = 0.8
mousepress = None
currently_dragging = False
current_artist = None
offset = [0,0]
n = 0
line_object = None
#------------------------------------------------
def on_press(event):
global currently_dragging
global mousepress
currently_dragging = True
if event.button == 3:
mousepress = "right"
elif event.button == 1:
mousepress = "left"
#------------------------------------------------
def on_release(event):
global current_artist, currently_dragging
current_artist = None
currently_dragging = False
#------------------------------------------------
def on_pick(event):
global current_artist, offset, n
global listLabelPoints
if current_artist is None:
current_artist = event.artist
#print("pick ", current_artist)
if isinstance(event.artist, patches.Circle):
if event.mouseevent.dblclick:
if mousepress == "right":
#print("double click right")
if len(ax.patches) > 2:
#print("\ndelete", event.artist.get_label())
event.artist.remove()
xdata = list(line_object[0].get_xdata())
ydata = list(line_object[0].get_ydata())
for i in range(0,len(xdata)):
if event.artist.get_label() == listLabelPoints[i]:
xdata.pop(i)
ydata.pop(i)
listLabelPoints.pop(i)
break
#print('--->', listLabelPoints)
line_object[0].set_data(xdata, ydata)
plt.draw()
else:
x0, y0 = current_artist.center
x1, y1 = event.mouseevent.xdata, event.mouseevent.ydata
offset = [(x0 - x1), (y0 - y1)]
elif isinstance(event.artist, Line2D):
if event.mouseevent.dblclick:
if mousepress == "left":
#print("double click left")
n = n+1
x, y = event.mouseevent.xdata, event.mouseevent.ydata
newPointLabel = "point"+str(n)
point_object = patches.Circle([x, y], radius=50, color='r', fill=False, lw=2,
alpha=point_alpha_default, transform=ax.transData, label=newPointLabel)
point_object.set_picker(5)
ax.add_patch(point_object)
xdata = list(line_object[0].get_xdata())
ydata = list(line_object[0].get_ydata())
#print('\ninit', listLabelPoints)
pointInserted = False
for i in range(0,len(xdata)-1):
#print("--> testing inclusion %s in [%s-%s]"
# %(newPointLabel, listLabelPoints[i], listLabelPoints[i+1]))
#print('----->', min(xdata[i],xdata[i+1]), '<', x, '<', max(xdata[i],xdata[i+1]))
#print('----->', min(ydata[i],ydata[i+1]), '<', y, '<', max(ydata[i],ydata[i+1]))
if x > min(xdata[i],xdata[i+1]) and x < max(xdata[i],xdata[i+1]) and \
y > min(ydata[i],ydata[i+1]) and y < max(ydata[i],ydata[i+1]) :
xdata.insert(i+1, x)
ydata.insert(i+1, y)
listLabelPoints.insert(i+1, newPointLabel)
pointInserted = True
#print("include", newPointLabel)
break
line_object[0].set_data(xdata, ydata)
#print('final', listLabelPoints)
plt.draw()
if not pointInserted:
print("Error: point not inserted")
else:
xdata = event.artist.get_xdata()
ydata = event.artist.get_ydata()
x1, y1 = event.mouseevent.xdata, event.mouseevent.ydata
offset = xdata[0] - x1, ydata[0] - y1
#------------------------------------------------
def on_motion(event):
global current_artist
if not currently_dragging:
return
if current_artist == None:
return
if event.xdata == None:
return
dx, dy = offset
if isinstance(current_artist, patches.Circle):
cx, cy = event.xdata + dx, event.ydata + dy
current_artist.center = cx, cy
#print("moving", current_artist.get_label())
xdata = list(line_object[0].get_xdata())
ydata = list(line_object[0].get_ydata())
for i in range(0,len(xdata)):
if listLabelPoints[i] == current_artist.get_label():
xdata[i] = cx
ydata[i] = cy
break
line_object[0].set_data(xdata, ydata)
elif isinstance(current_artist, Line2D):
xdata = list(line_object[0].get_xdata())
ydata = list(line_object[0].get_ydata())
xdata0 = xdata[0]
ydata0 = ydata[0]
for i in range(0,len(xdata)):
xdata[i] = event.xdata + dx + xdata[i] - xdata0
ydata[i] = event.ydata + dy + ydata[i] - ydata0
line_object[0].set_data(xdata, ydata)
for p in ax.patches:
pointLabel = p.get_label()
i = listLabelPoints.index(pointLabel)
p.center = xdata[i], ydata[i]
plt.draw()
#------------------------------------------------
def on_click(event):
global n, line_object
if event and event.dblclick:
if len(listLabelPoints) < 2:
n = n+1
x, y = event.xdata, event.ydata
newPointLabel = "point"+str(n)
point_object = patches.Circle([x, y], radius=50, color='r', fill=False, lw=2,
alpha=point_alpha_default, transform=ax.transData, label=newPointLabel)
point_object.set_picker(5)
ax.add_patch(point_object)
listLabelPoints.append(newPointLabel)
if len(listLabelPoints) == 2:
xdata = []
ydata = []
for p in ax.patches:
cx, cy = p.center
xdata.append(cx)
ydata.append(cy)
line_object = ax.plot(xdata, ydata, alpha=0.5, c='r', lw=2, picker=True)
line_object[0].set_pickradius(5)
plt.draw()
#================================================
fig, ax = plt.subplots()
ax.set_title("Double click left button to create draggable point\nDouble click right to remove a point", loc="left")
ax.set_xlim(0, 4000)
ax.set_ylim(0, 3000)
ax.set_aspect('equal')
fig.canvas.mpl_connect('button_press_event', on_click)
fig.canvas.mpl_connect('button_press_event', on_press)
fig.canvas.mpl_connect('button_release_event', on_release)
fig.canvas.mpl_connect('pick_event', on_pick)
fig.canvas.mpl_connect('motion_notify_event', on_motion)
plt.grid(True)
plt.show()
I needed more points in the graph, so I modified the solution done by JPFrancoia in order to have more points connected with lines. I hope somebody else could find it usefull, so the new drag.py file reads:
# drag.py
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.lines import Line2D
class DraggablePoint:
# http://stackoverflow.com/questions/21654008/matplotlib-drag-overlapping-points-interactively
lock = None # only one can be animated at a time
def __init__(self, parent, x=0.1, y=0.1, size=0.1):
self.parent = parent
self.point = patches.Ellipse((x, y), size, size * 3, fc='r', alpha=0.5, edgecolor='r')
self.x = x
self.y = y
parent.fig.axes[0].add_patch(self.point)
self.press = None
self.background = None
self.connect()
# if another point already exist we draw a line
if self.parent.list_points:
line_x = [self.parent.list_points[-1].x, self.x]
line_y = [self.parent.list_points[-1].y, self.y]
self.line = Line2D(line_x, line_y, color='r', alpha=0.5)
parent.fig.axes[0].add_line(self.line)
def connect(self):
'connect to all the events we need'
self.cidpress = self.point.figure.canvas.mpl_connect('button_press_event', self.on_press)
self.cidrelease = self.point.figure.canvas.mpl_connect('button_release_event', self.on_release)
self.cidmotion = self.point.figure.canvas.mpl_connect('motion_notify_event', self.on_motion)
def on_press(self, event):
if event.inaxes != self.point.axes: return
if DraggablePoint.lock is not None: return
contains, attrd = self.point.contains(event)
if not contains: return
self.press = (self.point.center), event.xdata, event.ydata
DraggablePoint.lock = self
# draw everything but the selected rectangle and store the pixel buffer
canvas = self.point.figure.canvas
axes = self.point.axes
self.point.set_animated(True)
# TODO also the line of some other points needs to be released
point_number = self.parent.list_points.index(self)
if self == self.parent.list_points[0]:
self.parent.list_points[1].line.set_animated(True)
elif self == self.parent.list_points[-1]:
self.line.set_animated(True)
else:
self.line.set_animated(True)
self.parent.list_points[point_number+1].line.set_animated(True)
canvas.draw()
self.background = canvas.copy_from_bbox(self.point.axes.bbox)
# now redraw just the rectangle
axes.draw_artist(self.point)
# and blit just the redrawn area
canvas.blit(axes.bbox)
def on_motion(self, event):
if DraggablePoint.lock is not self:
return
if event.inaxes != self.point.axes: return
self.point.center, xpress, ypress = self.press
dx = event.xdata - xpress
dy = event.ydata - ypress
self.point.center = (self.point.center[0]+dx, self.point.center[1]+dy)
canvas = self.point.figure.canvas
axes = self.point.axes
# restore the background region
canvas.restore_region(self.background)
# redraw just the current rectangle
axes.draw_artist(self.point)
point_number = self.parent.list_points.index(self)
self.x = self.point.center[0]
self.y = self.point.center[1]
# We check if the point is A or B
if self == self.parent.list_points[0]:
# or we draw the other line of the point
self.parent.list_points[1].line.set_animated(True)
axes.draw_artist(self.parent.list_points[1].line)
elif self == self.parent.list_points[-1]:
# we draw the line of the point
axes.draw_artist(self.line)
else:
# we draw the line of the point
axes.draw_artist(self.line)
#self.parent.list_points[point_number+1].line.set_animated(True)
axes.draw_artist(self.parent.list_points[point_number+1].line)
if self == self.parent.list_points[0]:
# The first point is especial because it has no line
line_x = [self.x, self.parent.list_points[1].x]
line_y = [self.y, self.parent.list_points[1].y]
# this is were the line is updated
self.parent.list_points[1].line.set_data(line_x, line_y)
elif self == self.parent.list_points[-1]:
line_x = [self.parent.list_points[-2].x, self.x]
line_y = [self.parent.list_points[-2].y, self.y]
self.line.set_data(line_x, line_y)
else:
# The first point is especial because it has no line
line_x = [self.x, self.parent.list_points[point_number+1].x]
line_y = [self.y, self.parent.list_points[point_number+1].y]
# this is were the line is updated
self.parent.list_points[point_number+1].line.set_data(line_x, line_y)
line_x = [self.parent.list_points[point_number-1].x, self.x]
line_y = [self.parent.list_points[point_number-1].y, self.y]
self.line.set_data(line_x, line_y)
# blit just the redrawn area
canvas.blit(axes.bbox)
def on_release(self, event):
'on release we reset the press data'
if DraggablePoint.lock is not self:
return
self.press = None
DraggablePoint.lock = None
# turn off the rect animation property and reset the background
self.point.set_animated(False)
point_number = self.parent.list_points.index(self)
if self == self.parent.list_points[0]:
self.parent.list_points[1].line.set_animated(False)
elif self == self.parent.list_points[-1]:
self.line.set_animated(False)
else:
self.line.set_animated(False)
self.parent.list_points[point_number+1].line.set_animated(False)
self.background = None
# redraw the full figure
self.point.figure.canvas.draw()
self.x = self.point.center[0]
self.y = self.point.center[1]
def disconnect(self):
'disconnect all the stored connection ids'
self.point.figure.canvas.mpl_disconnect(self.cidpress)
self.point.figure.canvas.mpl_disconnect(self.cidrelease)
self.point.figure.canvas.mpl_disconnect(self.cidmotion)
And the excution is done like:
import sys
import matplotlib
matplotlib.use("Qt5Agg")
from PyQt5 import QtWidgets, QtGui
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
# Personnal modules
from drag import DraggablePoint
class MyGraph(FigureCanvas):
"""A canvas that updates itself every second with a new plot."""
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = self.fig.add_subplot(111)
self.axes.grid(True)
FigureCanvas.__init__(self, self.fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
# To store the 2 draggable points
self.list_points = []
self.show()
self.plotDraggablePoints()
def plotDraggablePoints(self, size=0.05):
"""Plot and define the 2 draggable points of the baseline"""
# del(self.list_points[:])
self.list_points.append(DraggablePoint(self, 0.1, 0.1, size))
self.list_points.append(DraggablePoint(self, 0.2, 0.2, size))
self.list_points.append(DraggablePoint(self, 0.5, 0.5, size))
self.list_points.append(DraggablePoint(self, 0.6, 0.5, size))
self.list_points.append(DraggablePoint(self, 0.7, 0.5, size))
self.updateFigure()
def clearFigure(self):
"""Clear the graph"""
self.axes.clear()
self.axes.grid(True)
del(self.list_points[:])
self.updateFigure()
def updateFigure(self):
"""Update the graph. Necessary, to call after each plot"""
self.draw()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = MyGraph()
sys.exit(app.exec_())