1

I'm trying to draw a line between two draggable points that are on a matplotlib canvas, embedded in a pyqt5 application. I'm using Draggable line with draggable points as a reference. However I'm trying to create multiple lines, with a pair of DraggablePoint objects being created on a button-click. Several problems are occuring for me:

  1. The line only appears when the points are being dragged (solved, see update below)
  2. The line is offset by a margin, which I assume is the difference between the canvas and the actual matplotlib plot. However the scaling isn't right either.
  3. The line for the 2nd point-pair is not showing at all

Below is my adapted code:

DraggablePoint

import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.lines import Line2D
import matplotlib


class DraggablePoint:



    lock = None 

    def __init__(self, parent, dominant, x=10, y=10, size=1):
        """Creates a draggable Point on a matplotlib canvas"""
        matplotlib.matplotlib_fname()
        # The FigureCanvas
        self.parent = parent
        # The Point
        self.point = patches.Ellipse((x, y), size, size, fc='r', alpha=0.5, edgecolor='r')
        #Coordinates of the point
        self.x = x
        self.y = y
        self.dy = 645
        self.dx = 263
        # Adds the point to the Plot
        parent.fig.axes[0].add_patch(self.point)
        # Used in the on_press() function
        self.press = None
        self.background = None
        # initiate the mpl_connects
        self.connect()
        # The Other DraggablePoint, with whom the line shall connect with.
        self.partner = None
        # The Line2D
        self.line = None
        # TODO
        self.dominant = dominant

        for pair in self.parent.point_pairs:
            if self in pair:
                if pair[1]:
                    line_x = [pair[0].x+self.dx, pair[1].x+self.dx]
                    line_y = [pair[0].y+self.dy, pair[1].y+self.dy]
                    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'
        # print("LOG.INFO: DraggablePoint.connect")
        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):
        '''Initiates when a Point is clicked on'''
        # print(self.partner)
        # print(event.xdata, event.ydata)
        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)
        for pair in self.parent.point_pairs:
            if self == pair[1]:
                self.line.set_animated(True)
            elif self == pair[0]:
                self.partner.line.set_animated(True)


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

        # print("LOG.INFO: DraggablePoint.on_motion")
        if DraggablePoint.lock is not self:
            return
        if event.inaxes != self.point.axes: return
        # print("LOG.INFO: DraggablePoint.on_motion.after_lock")
        # self.parent.updateFigure()
        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)

        for pair in self.parent.point_pairs:
            if self in pair:
                if self == pair[1]:
                    axes.draw_artist(self.line)
                else:
                    pair[1].line.set_animated(True)
                    axes.draw_artist(pair[1].line)



        self.x = self.point.center[0]
        self.y = self.point.center[1]

        for pair in self.parent.point_pairs:
            if self == pair[1]:
                line_x = [pair[0].x+self.dx, self.x+self.dx]
                line_y = [pair[0].y+self.dy, self.y+self.dy]
                self.line.set_data(line_x, line_y)
            elif self == pair[0]:
                line_x = [pair[1].x+self.dx, self.x+self.dx]
                line_y = [pair[1].y+self.dy, self.y+self.dy]
                pair[1].line.set_data(line_x, line_y)

        # blit just the redrawn area
        canvas.blit(axes.bbox)
        # print(self.line)


    def on_release(self, event):

        # print("LOG.INFO: DraggablePoint.on_release")
        'on release we reset the press data'
        if DraggablePoint.lock is not self:
            return

        # print("LOG.INFO: DraggablePoint.on_release.after_lock")
        self.press = None
        DraggablePoint.lock = None

        # turn off the rect animation property and reset the background
        self.point.set_animated(False)
        axes = self.point.axes

        for pair in self.parent.point_pairs:
            if self in pair:
                if pair[1] == self:
                    self.line.set_animated(False)
                else:
                    pair[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]
        print(self.line.__str__() + "RELEASE")

    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)

    def setLine(self, line):
        self.line = line

Embedding in the GUI

class PlotCanvas(FigureCanvas):

    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)

        FigureCanvas.__init__(self, self.fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QSizePolicy.Expanding,
                                   QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)
        self.xcoords = []
        self.ycoords = []
        self.lines = []
        self.list_points = []
        self.point_pairs = []
        self.number_of_lines = 0
        # self.mpl_connect('button_press_event', self.plot_draggable_point)
        self.plot_line = False
        self.plot()
        self.create_draggable_points()


    def plot(self):
        # data = [random.random() for i in range(25)]

        # self.a = plt.scatter(M[:, 0], M[:, 1])
        data = dataset2.get_matrix()
        ax = self.figure.add_subplot(111)
        ax.scatter(data[:, 0], data[:, 1], picker=10)
        ax.set_title('PyQt Matplotlib Example')
        self.draw()

    def create_draggable_points(self):
        self.list_points.append(DraggablePoint(self, True, 618.5047115210559, 338.5460575139148, 20))
        self.list_points.append(DraggablePoint(self, False, 338.5460575139148, 118.5047115210559, 20))
        # TODO Koordinaten an den Plot anpassen (+500)
        i = self.list_points[0]
        j = self.list_points[1]
        i.partner = j
        j.partner = i
        i.setLine(Line2D([i.x, j.x], [i.y, j.y], color='r', alpha=0.5))
        j.setLine(Line2D([i.x, j.x], [i.y, j.y], color='r', alpha=0.5))
        self.lines.append(i.line)
        self.lines.append(j.line)
        print(self.lines)

        self.point_pairs.append((i, j))

        self.updateFigure()


    def plot_draggable_point(self, event, size=60):
        if self.plot_line:
            self.xcoords.append(event.xdata)
            self.ycoords.append(event.ydata)
            print(event.xdata)
            print(event.ydata)
            self.list_points.append(DraggablePoint(self, event.xdata, event.ydata, size))
            if len(self.xcoords) == 2:
                self.list_points.append(DraggablePoint(self, event.xdata, event.ydata, size))
                self.xcoords[:] = []
                self.ycoords[:] = []
                self.list_points[:] = []
                self.plot_line = False
            self.updateFigure()

    def updateFigure(self):
        print(self.point_pairs)
        self.draw()

The class gets called this way:

    layout = QGridLayout()
    self.m = PlotCanvas(self, width=10, height=8)
    layout.addWidget(self.m, 0, 0, 5, 1)

Update:

The first issue has been solved, I forgot to redraw the line in the on_release() method. It now looks like this:

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)
    axes = self.point.axes

    for pair in self.parent.point_pairs:
        if self in pair:
            if pair[1] == self:
                self.line.set_animated(False)
            else:
                pair[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]


    for pair in self.parent.point_pairs:
        if self in pair:
            if pair[1] == self:
                axes.draw_artist(self.line)
            else:
                axes.draw_artist(pair[1].line)
Marcel Baur
  • 21
  • 1
  • 6

1 Answers1

1

I have solved the problem of question 2 by introducing scaling and offset. The other two problems were due to the canvas being redrawn over the created lines. Those problems are now fixed. The class for DraggablePoint now looks like this:

import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.lines import Line2D
import matplotlib


class DraggablePoint:



    lock = None

    def __init__(self, parent, dominant, x=10, y=10, size=1):
        """Creates a draggable Point on a matplotlib canvas"""
        matplotlib.matplotlib_fname()
        # The FigureCanvas
        self.parent = parent
        # The Point
        self.point = patches.Ellipse((x, y), size, size, fc='r', alpha=0.5, edgecolor='r')
        #Coordinates of the point
        self.x = x
        self.y = y
        self.x_offset = 260
        self.y_offset = 640
        self.dy = 640
        self.dx = 260
        self.x_offset_factor = 0.06
        self.y_offset_factor = -0.089
        self.x_scaling = self.x * self.x_offset_factor
        self.y_scaling = self.y * self.y_offset_factor
        self.dy = self.y_offset + self.y_scaling
        self.dx = self.x_offset + self.x_scaling
        # Adds the point to the Plot
        parent.fig.axes[0].add_patch(self.point)
        # Used in the on_press() function
        self.press = None
        self.background = None
        # initiate the mpl_connects
        self.connect()
        # The Other DraggablePoint, with whom the line shall connect with.
        self.partner = None
        # The Line2D
        self.line = None
        self.dominant = dominant

        for pair in self.parent.point_pairs:
            if self in pair:
                if self == pair[1]:
                    line_x = [pair[0].x + pair[0].dx, self.x+self.dx]
                    line_y = [pair[0].y + pair[0].dy, self.y+self.dy]
                    self.line = Line2D(line_x, line_y, color='r', alpha=0.5)
                    parent.fig.axes[0].add_line(self.line)
                else:
                    line_x = [pair[1].x + pair[1].dx, self.x + self.dx]
                    line_y = [pair[1].y + pair[1].dy, self.y + self.dy]
                    self.line = Line2D(line_x, line_y, color='r', alpha=0.5)
                    parent.fig.axes[0].add_line(self.line)
        for pair in self.parent.point_pairs:
            self.point.axes.draw_artist(pair[1].line)


    def connect(self):

        'connect to all the events we need'
        # print("LOG.INFO: DraggablePoint.connect")
        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):
        '''Initiates when a Point is clicked on'''
        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)
        for pair in self.parent.point_pairs:
            if self == pair[1]:
                self.line.set_animated(True)
            elif self == pair[0]:
                self.partner.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):

        # print("LOG.INFO: DraggablePoint.on_motion")
        if DraggablePoint.lock is not self:
            return
        if event.inaxes != self.point.axes: return
        # print("LOG.INFO: DraggablePoint.on_motion.after_lock")
        # self.parent.updateFigure()
        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)

        #Update the scaling of the offset
        self.x_scaling = self.x * self.x_offset_factor
        self.y_scaling = self.y * self.y_offset_factor
        self.dy = self.y_offset + self.y_scaling
        self.dx = self.x_offset + self.x_scaling

        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)

        for pair in self.parent.point_pairs:
            if self in pair:
                axes.draw_artist(pair[1].line)
            if self == pair[1]:
                self.x_scaling = self.x * self.x_offset_factor
                self.y_scaling = self.y * self.y_offset_factor
                self.dy = self.y_offset + self.y_scaling
                self.dx = self.x_offset + self.x_scaling



        self.x = self.point.center[0]
        self.y = self.point.center[1]

        for pair in self.parent.point_pairs:
            if self == pair[1]:
                line_x = [pair[0].x + pair[0].dx, self.x+self.dx]
                line_y = [pair[0].y + pair[0].dy, self.y+self.dy]
                self.line.set_data(line_x, line_y)
            elif self == pair[0]:
                line_x = [pair[1].x + pair[1].dx, self.x+self.dx]
                line_y = [pair[1].y + pair[1].dy, self.y+self.dy]
                pair[1].line.set_data(line_x, line_y)

        # blit just the redrawn area
        canvas.blit(axes.bbox)
        # print(self.line)


    def on_release(self, event):

        # print("LOG.INFO: DraggablePoint.on_release")
        'on release we reset the press data'
        if DraggablePoint.lock is not self:
            return

        # print("LOG.INFO: DraggablePoint.on_release.after_lock")
        self.press = None
        DraggablePoint.lock = None

        # turn off the rect animation property and reset the background
        self.point.set_animated(False)
        axes = self.point.axes

        for pair in self.parent.point_pairs:
            if self in pair:
                if pair[1] == self:
                    self.line.set_animated(False)
                else:
                    pair[1].line.set_animated(False)

        print(self.x_scaling, self.y_scaling)


        self.background = None

        # redraw the full figure
        self.point.figure.canvas.draw()

        self.x = self.point.center[0]
        self.y = self.point.center[1]

        for pair in self.parent.point_pairs:
            axes.draw_artist(pair[1].line)
        print(self.line.__str__() + "RELEASE")

    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)

    def setLine(self, line):
        self.line = line
Marcel Baur
  • 21
  • 1
  • 6