Related
I am looking to annotate a draggable circle with a number. The draggeable point is created from this class:
class DraggablePoint:
lock = None #only one can be animated at a time
def __init__(self, point):
self.point = point
self.press = None
self.background = None
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 point 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()
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)
I am plotting with this (circles.append + the last loop is the interesting code for this problem):
drs = []
circles = []
for team, color, sec_color in zip([hometeam.loc[frame], awayteam.loc[frame]], team_colors, sec_colors):
x_columns = [c for c in team.keys() if
c[-2:].lower() == '_x' and c != 'ball_x'] # column header for player x positions
y_columns = [c for c in team.keys() if
c[-2:].lower() == '_y' and c != 'ball_y'] # column header for player y positions
for x, y in zip(team[x_columns], team[y_columns]):
#if ctr == 0:
circles.append(patches.Circle((x, y), 1.4, fc=color, edgecolor=sec_color, linewidth=1.2, zorder=2))
for circ in circles:
ax.add_patch(circ)
dr = DraggablePoint(circ)
dr.connect()
drs.append(dr)
plt.show()
I can plot text at the circles center, however once I drag a circle, the text stays at the original position. Is there a way to add text to the circles, which would move alongside the circles when dragged?
Sure. Add a label, record the relative position to the circle, and update its position on motion.
import matplotlib.pyplot as plt
class DraggablePoint:
...
class LabelledDraggablePoint(DraggablePoint):
def __init__(self, point, label):
super().__init__(point)
self.label = label
x1, y1 = self.label.get_position()
x2, y2 = self.point.center
self.label_offset = (x1 - x2, y1 - y2)
def on_motion(self, event):
super().on_motion(event)
self.label.set_position(self.point.center + self.label_offset)
if __name__ == '__main__':
fig, ax = plt.subplots()
point = plt.Circle((0.5, 0.5), 0.1)
ax.add_patch(point)
ax.set_aspect('equal')
label = ax.text(0.5, 0.5, 'Lorem ipsum', ha='center', va='center')
instance = LabelledDraggablePoint(point, label)
instance.connect()
plt.show()
On a different note: the blitting is nausea inducing. You will get much smoother animations without it. I would cut all of that out.
I have been looking around for a while and cant seem to find much on moving elements around after they have been plotted. I have a series of vertical lines plotted and if they are to close together i would like to be able to space then out a bit more. The issue is that they cant be moved to the left ever. I have code that can evenly space all these with that constraint but now I0m focusing on just making sure they are not clumped together. here is an example picture of what I'm working with:
full view
zoomed in on a problem
Thee question really is if there is a way I am able to click and drag these red lines around so they are not to close to others? i need to be able to retrieve the new positions of all the lines after this is done after i have made them all nicely spaced but i assume this would be fairly simple after i have this mechanic in place?
I'm not looking for specific implementation just some help on places I could look to be able to make this click and drag utility possible.
This may not be possible in matplotlib itself and i may have to look outward into making some GUI to do this but i have no experience in this so probably not the best solution although probably the best.
Any insight into how I might be able to achieve the click drag utility will be greatly appreciated!
-Thank you
got it working from a movable polygon example here: https://matplotlib.org/stable/gallery/event_handling/poly_editor.html
import numpy as np
import pandas as pd
from matplotlib.lines import Line2D
from matplotlib.artist import Artist
global new_freqs
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://www.geomalgorithms.com/algorithms.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 PolygonInteractor:
"""
A polygon editor.
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, start_freqs):
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
x, y = zip(*self.poly.xy)
self.line = Line2D(x, y,
marker='o', markerfacecolor='r',
animated=True)
self.ax.add_line(self.line)
ax.vlines(x,linestyle="--", ymin=0.0, ymax=1, alpha=0.9, color=col, gid="new_lines")
self.original_freqs = start_freqs #variable for storing the starting frequencies
self.orig_cols = col # array or starting colours
self.new_cols = self.orig_cols # array or new colours, same as original to begin with
self.issue_spacing = 1.0e6 # variable to store the value of kids to close together
self.cid = self.poly.add_callback(self.poly_changed)
self._ind = None # the active vert
canvas.mpl_connect('draw_event', self.on_draw)
canvas.mpl_connect('button_press_event', self.on_button_press)
canvas.mpl_connect('key_press_event', self.on_key_press)
canvas.mpl_connect('button_release_event', self.on_button_release)
canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
self.canvas = canvas
def draw_new_positions(self):
for i in range(len(self.new_cols)):
if self.poly.xy[i,0] != self.original_freqs[i]:
if self.poly.xy[i,0] < self.original_freqs[i]:
self.new_cols[i] = "purple" #if the kid has moved backwward show purple
elif (self.poly.xy[i+1,0]-self.poly.xy[i,0]) < self.issue_spacing or (self.poly.xy[i,0]-self.poly.xy[i-1,0]) < self.issue_spacing :
self.new_cols[i] = "black" #if the kid to close the the ones next to it show black
else:
self.new_cols[i] = "orange" #if the kid has moved and is positioned ok show orange
else:
self.new_cols[i] = self.orig_cols[i]
new_lines = self.ax.vlines(self.poly.xy[:-1,0], ymin=-1, ymax=0, linestyle="--", color=self.new_cols, alpha=0.9, gid="new_lines") #new line to move where mouse is
self.ax.draw_artist(new_lines) # drawing the line on moving the mouse
self.canvas.blit(self.ax.bbox) # blitting the canvas to render moving
new_lines.remove()
def on_draw(self, event):
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.draw_artist(self.poly)
self.ax.draw_artist(self.line)
# 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 pathpatch 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):
"""
Return the index of the point closest to the event position or *None*
if no point is within ``self.epsilon`` to the event position.
"""
# 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 on_button_press(self, event):
"""Callback for mouse button presses."""
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 on_button_release(self, event):
"""Callback for mouse button releases."""
if not self.showverts:
return
if event.button != 1:
return
self._ind = None
def on_key_press(self, event):
"""Callback for key presses."""
if not event.inaxes:
return
if event.key == 't': #toggles the movable points on and off
self.showverts = not self.showverts
self.line.set_visible(self.showverts)
if not self.showverts:
self._ind = None
elif event.key == ' ': #prints the x vals of all polygon points (which are the new frequencies) to the console
new_freqs = self.poly.xy[:,0]
for i in range(len(new_freqs)-1):
print("{:.1f},".format(new_freqs[i]))
# print(len(new_freqs))
elif event.key == 'l': #save new frequencies to csv file and show final plot
new_freqs = self.poly.xy[:-1,0]
new_data = 0
new_data = np.zeros((len(new_freqs), 2))
new_data[:,0] = data["kid_id"]
new_data[:,1] = new_freqs
new_data_df = pd.DataFrame(data=new_data, columns=["kid_id", "f0"]) #makes a new data frame to save to csv with all new positions
new_data_df.to_csv("new_kid_positions.csv", index=False)
plt.close()
plt.figure("new array", dpi=150)
for i in range(len(new_data_df["f0"])):
if self.poly.xy[i,0] == self.original_freqs[i]:
col="green"
else:
col="orange"
plt.axvline(new_data_df["f0"][i]/1e6, color=col, linestyle="--", linewidth=1.5, alpha=0.9)
plt.plot([],[], color="orange", label="Moved")
plt.plot([],[], color="green", label="Not moved")
plt.legend(loc="best")
plt.xlabel("Frequency (MHz)")
plt.ylabel("")
plt.title("Altered array")
plt.grid()
plt.show()
if self.line.stale:
self.canvas.draw_idle()
def on_mouse_move(self, event):
"""Callback for mouse movements."""
if not self.showverts:
return
if self._ind is None:
return
if event.inaxes is None:
return
if event.button != 1:
self.moving_line.remove()
return
x, y = event.xdata, 0
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))
# self.remove_series()
# f = x
# self.add_series(f, "new_lines", len(self.poly.xy[:,0])-1)
# ax.axvline(x, ymin=-1, ymax=1, linestyle="--", color="orange", alpha=0.9, animated=True)
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.poly)
self.ax.draw_artist(self.line)
# self.moving_line = self.ax.axvline(x, ymin=-1, ymax=1, linestyle="--", color="orange", alpha=0.9) #new line to move where mouse is
# self.ax.draw_artist(self.moving_line) # drawing the line on moving the mouse
# self.moving_line.remove()
self.draw_new_positions()
self.canvas.blit(self.ax.bbox) # blitting the canvas to render moving
if __name__ == '__main__':
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
#reading in freequency positions
data = pd.read_csv("sorted_kids.csv")
#getting color for each line
col = [] #empty list
for i in range(len(data["issue"])):
if data["issue"][i] == 1:
col.append("red")
else:
col.append("green")
# col = np.array(col)
xs = data["f0"]
ys = np.zeros_like(xs)
poly = Polygon(np.column_stack([xs, ys]), animated=True)
fig, ax = plt.subplots(dpi=150, figsize=(12,6))
ax.add_patch(poly)
p = PolygonInteractor(ax, poly, xs)
ax.set_title('Click a point to drag. spacebar=print freqs. T=toggle move. L=save and show final solution')
ax.plot([],[], color="black", alpha=0.0, label=r"TOP: ORIGINAL ARRAY")
ax.plot([],[], color="red", label="to close")
ax.plot([],[], color="green", label="spacing ok")
ax.plot([],[], color="black", alpha=0.0, label="\nBOT: ALTEERED ARRAY")
ax.plot([],[], color="red", label="orig position & to close")
ax.plot([],[], color="green", label="orig position & ok")
ax.plot([],[], color="purple", label="moved backward!")
ax.plot([],[], color="black", label="to close still")
ax.plot([],[], color="orange", label="moved & ok")
ax.legend(loc=1, fontsize=6)
plt.xlabel("Frequency (Hz)")
ax.set_xlim((min(data["f0"])-10e6, max(data["f0"])+10e6))
ax.set_ylim((-1.5, 1.5))
plt.tight_layout(pad=0.4, w_pad=0.5, h_pad=1.0)
ax.axes.get_yaxis().set_visible(False)
plt.show()
When I load a 3840x2160 image with below code:
#!/usr/bin/env python3
import matplotlib.pyplot as plt
import matplotlib.image as image
import matplotlib.patches as patches
class SnaptoCursor(object):
def __init__(self, ax):
self.ax = ax
self.lx = ax.axhline(color='r') # the horiz line
self.ly = ax.axvline(color='r') # the vert line
self.x = 0
self.y = 0
def mouse_move(self, event):
if not event.inaxes:
return
x, y = event.xdata, event.ydata
# update the line positions
self.lx.set_ydata(y)
self.ly.set_xdata(x)
self.ax.figure.canvas.draw()
img = image.imread("exam.jpg") #3840x2160
fig,ax =plt.subplots(1)
ax.imshow(img)
snap_cursor = SnaptoCursor(ax)
fig.canvas.mpl_connect('motion_notify_event', snap_cursor.mouse_move)
plt.show()
When I move the mouse, I can see the long crosshair cursor (two line actually) move very slow. I wish it move with the cursor immediately.
Welcome if any other good solution!
You are redrawing the entire canvas every time the mouse moves, this is unnecessary - you simply need to redraw the two artists and use blitting to avoid redrawing the rest. This is slightly tricky when there is something behind the relevant artists (i.e. an image or colormesh). Something like this should suffice:
class SnaptoCursor(object):
def __init__(self, ax):
# Have to draw the canvas once beforehand to cache the renderer
ax.figure.canvas.draw()
self.bg = ax.figure.canvas.copy_from_bbox(ax.bbox)
self.ax = ax
self.lx = ax.axhline(color='r') # the horiz line
self.ly = ax.axvline(color='r') # the vert line
self.x = 0
self.y = 0
def mouse_move(self, event):
if not event.inaxes:
return
x, y = event.xdata, event.ydata
# update the line positions
ax.figure.canvas.restore_region(self.bg)
self.lx.set_ydata(y)
self.ly.set_xdata(x)
self.ax.draw_artist(self.lx)
self.ax.draw_artist(self.ly)
self.ax.figure.canvas.blit(self.ax.bbox)
Although adjustments may be required depending on the backend etc.
I've got the following code that produces a plot that can interactively be modified. Clicking / holding the left mouse button sets the marker position, Holding the right button and moving the mouse moves the plotted data in direction x and using the mouse wheel zooms in/out. Additionally, resizing the window calls figure.tight_layout() so that the size of the axes is adapted to the window size.
# coding=utf-8
from __future__ import division
from Tkinter import *
import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from numpy import arange, sin, pi
matplotlib.use('TkAgg')
class PlotFrame(Frame):
def __init__(self, master, **ops):
Frame.__init__(self, master, **ops)
self.figure = Figure()
self.axes_main = self.figure.add_subplot(111)
for i in range(10):
t = arange(0, 300, 0.01)
s = sin(0.02 * pi * (t + 10 * i))
self.axes_main.plot(t, s)
self.plot = FigureCanvasTkAgg(self.figure, master=self)
self.plot.show()
self.plot.get_tk_widget().pack(fill=BOTH, expand=1)
self.dragging = False
self.dragging_button = None
self.mouse_pos = [0, 0]
self.marker = self.figure.axes[0].plot((0, 0), (-1, 1), 'black', linewidth=3)[0]
self.plot.mpl_connect('button_press_event', self.on_button_press)
self.plot.mpl_connect('button_release_event', self.on_button_release)
self.plot.mpl_connect('motion_notify_event', self.on_mouse_move)
self.plot.mpl_connect('scroll_event', self.on_mouse_scroll)
self.plot.mpl_connect("resize_event", self.on_resize)
def on_resize(self, _):
self.figure.tight_layout()
def axes_size(self):
pos = self.axes_main.get_position()
bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
axis_size = [(pos.x1 - pos.x0) * width, (pos.y1 - pos.y0) * height]
return axis_size
def on_button_press(self, event):
# right mouse button clicked
if not self.dragging and event.button in (1, 3):
self.dragging = True
self.dragging_button = event.button
self.mouse_pos = [event.x, event.y]
# left mouse button clicked
if event.button == 1 and event.xdata is not None:
self.move_marker(event.xdata)
def on_button_release(self, event):
if self.dragging and self.dragging_button == event.button:
self.dragging = False
def on_mouse_move(self, event):
if self.dragging and self.dragging_button == 3:
dx = event.x - self.mouse_pos[0]
self.mouse_pos = [event.x, event.y]
x_min, x_max = self.figure.axes[0].get_xlim()
x_range = x_max - x_min
x_factor = x_range / self.axes_size()[0]
self.figure.axes[0].set_xlim([x_min - dx * x_factor, x_max - dx * x_factor])
self.plot.draw()
elif self.dragging and self.dragging_button == 1:
self.move_marker(event.xdata)
def on_mouse_scroll(self, event):
if event.xdata is None:
return
zoom_direction = -1 if event.button == 'up' else 1
zoom_factor = 1 + .4 * zoom_direction
x_min, x_max = self.figure.axes[0].get_xlim()
min = event.xdata + (x_min - event.xdata) * zoom_factor
max = event.xdata + (x_max - event.xdata) * zoom_factor
self.figure.axes[0].set_xlim([min, max])
self.plot.draw()
def move_marker(self, x_position):
y_min, y_max = self.figure.axes[0].get_ylim()
self.marker.set_data((x_position, x_position), (y_min, y_max))
self.plot.draw()
if __name__ == '__main__':
gui = Tk()
vf = PlotFrame(gui)
vf.pack(fill=BOTH, expand=1)
gui.mainloop()
The implementation works fine, but rendering is really slow when displaying a lot of lines. How can I make rendering faster? As you can see in the implementation above, the whole plot is drawn completely every time anything changes which shouldn't be necessary. My thoughts on this:
Resizing the window: draw everything
Zooming: draw everything
Moving the marker: just redraw the marker (one line) instead of drawing everything
Moving the plot in x direction: move the pixels currently displayed in the plot left/right and only draw pixels that are moved into the visible area
Drawing everything when resizing/zooming is fine for me, but I really need faster drawing of the latter two modifications. I already looked into matplotlib's animations, but as far as I understood, they won't help in my case. Any help is greatly appreciated, thanks!
The solution seems to be to cache elements that get redrawn as you said:
One major thing that gets redrawn is the background:
# cache the background
background = fig.canvas.copy_from_bbox(ax.bbox)
After caching restore it using restore region then just re-draw the points/line at every call you need
# restore background
fig.canvas.restore_region(background)
# redraw just the points
ax.draw_artist(points)
# fill in the axes rectangle
fig.canvas.blit(ax.bbox)
To optimize drawing blitting can be used. With it only given artists (those that were changed) will be rendered instead of the whole figure.
Motplotlib uses that technique internally in the animation module. You can use Animation class in it as a reference to implement the same behaviour in your code. Look at the _blit_draw() and several related functions after it in the sources.
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_())