0

I'm trying to use matplotlib inside PyQt5 GUI, and be able to move points by clicking on a point and after that on the place i want to move it, and for that i use FuncAnimation. I want to be able to see some details on the point before i'm picking it when hovering the point.

I found the mplcursors library and tried it, but because i uses the FuncAnimation option i can't make it work together. Also, because i'm not using scatter i couldn't understood how to use the mpld3 library. I tried to set the blit option in the animation to False but none off the things above worked for me.

This is how my code looks like:

import sys
import matplotlib

matplotlib.use('Qt5Agg')
import mplcursors
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QApplication, QMainWindow, QMenu, QVBoxLayout, QHBoxLayout, QSizePolicy, QWidget, \
    QTextBrowser, QLineEdit
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.animation import FuncAnimation
from Entities import Soldier, CompanyCommander, BTW, FieldObjects


class MyMplCanvas(FigureCanvas):
    fig = Figure(figsize=(5, 4), dpi=200)
    ax = fig.add_subplot(1, 1, 1)
    def __init__(self, parent=None):

        FigureCanvas.__init__(self, MyMplCanvas.fig)
        self.setParent(parent)
        FigureCanvas.setSizePolicy(self,
                                   QtWidgets.QSizePolicy.Expanding,
                                   QtWidgets.QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)


class ApplicationWindow(QtWidgets.QMainWindow):
    soldiers = []
    picked_soldier = []

    s1 = Soldier(1, (3, 4), 100)
    s2 = Soldier(2, (5, 6), 100)
    s3 = Soldier(3, (1, 6), 100)
    s4 = BTW(1, (2, 3), 100)
    s5 = BTW(2, (3, 3.5), 100)
    s6 = BTW(3, (4.2, 3.7), 100)
    s7 = Soldier(1, (5.3, 4), 100)
    s8 = Soldier(2, (2.6, 4.3), 100)
    s9 = Soldier(3, (7, 5.2), 100)

    soldiers = [s1, s2, s3, s4, s5, s6, s7, s8, s9]

    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.setWindowTitle("application main window")
        self.main_widget = QtWidgets.QWidget(self)

        vbox = QtWidgets.QVBoxLayout(self.main_widget)

        self.canvas = MyMplCanvas(self.main_widget)  ###attention###
        vbox.addWidget(self.canvas)

        hbox = QtWidgets.QHBoxLayout(self.main_widget)

        self.setLayout(vbox)

        self.main_widget.setFocus()
        self.setCentralWidget(self.main_widget)

        self.ani = FuncAnimation(self.canvas.figure, self.animate, interval=1000, blit=False)
        self.curs = mplcursors.cursor(hover=True, highlight=True).connect("add", lambda sel: sel.annotation.set_text(sel.artist.get_label()))
        self.canvas.figure.canvas.mpl_connect('pick_event', ApplicationWindow.on_pick)

    def animate(self, i):
        self.canvas.ax.clear()
        self.create_plot()

    def create_plot(self):
        for s in self.soldiers:
            if s.company_number == 1:
                if type(s) == Soldier:
                    self.canvas.ax.plot(s.x, s.y, marker='o', markersize=5, color="blue", picker=5, label=s.__str__())
                else:
                    self.canvas.ax.plot(s.x, s.y, marker='*', markersize=5, color="blue", picker=5, label=s.__str__())

            elif s.company_number == 2:
                if type(s) == Soldier:
                    self.canvas.ax.plot(s.x, s.y, marker='o', markersize=5, color="red", picker=5, label=s.__str__())
                else:
                    self.canvas.ax.plot(s.x, s.y, marker='*', markersize=5, color="red", picker=5, label=s.__str__())

            elif s.company_number == 3:
                if type(s) == Soldier:
                    self.canvas.ax.plot(s.x, s.y, marker='o', markersize=5, color="green", picker=5, label=s.__str__())
                else:
                    self.canvas.ax.plot(s.x, s.y, marker='*', markersize=5, color="green", picker=5, label=s.__str__())
            else:
                continue


    def on_pick(event):
        this_point = event.artist
        x_data = this_point.get_xdata()
        y_data = this_point.get_ydata()
        ind = event.ind

        for soldier in ApplicationWindow.soldiers:
            if soldier.x == x_data and soldier.y == y_data:
                index = soldier.ID - 1
                ApplicationWindow.picked_soldier.append(soldier)
                break

        print(str(float(x_data[ind])) + ", " + str(float(y_data[ind])))
        print(str(ApplicationWindow.soldiers[index].__str__()))

        MyMplCanvas.fig.canvas.mpl_connect('button_press_event', ApplicationWindow.on_click)
        MyMplCanvas.fig.canvas.mpl_disconnect(MyMplCanvas.fig.canvas.mpl_connect
                                                           ('pick_event', ApplicationWindow.on_pick))

    def on_click(event):
        x_data = event.xdata
        y_data = event.ydata
        if len(ApplicationWindow.picked_soldier) > 0:
            soldier = ApplicationWindow.picked_soldier.pop(0)
            soldier.update_location(x_data, y_data)

        print(x_data, y_data)
        MyMplCanvas.fig.canvas.mpl_connect('pick_event', ApplicationWindow.on_pick)
        MyMplCanvas.fig.canvas.mpl_disconnect(MyMplCanvas.fig.canvas.mpl_connect('button_press_event', ApplicationWindow.on_click))

if __name__ == "__main__":
    App = QApplication(sys.argv)
    aw = ApplicationWindow()
    aw.show()
    sys.exit(App.exec_())

I would like to know if there is a better way the show the labels on hovering a point or what else i can try to make the labels work when usin FuncAnimation and PyQt5 GUI together.

Adi Portal
  • 55
  • 6
  • Possible duplicate of [Possible to make labels appear when hovering over a point in matplotlib?](https://stackoverflow.com/questions/7908636/possible-to-make-labels-appear-when-hovering-over-a-point-in-matplotlib) – musicamante Oct 01 '19 at 21:23
  • @musicamante I saw this question but the problem is that they using scatter in their plot, and i couldn't found how to fit it to my code – Adi Portal Oct 02 '19 at 07:22
  • I might have a solution, but before that I'd like to understand why you keep clearing and plotting the graph over and over. Can't you just add the markers once and then use the animation to move them (if you need to do so)? – musicamante Oct 02 '19 at 19:42
  • @musicamante Because the locations changes every time i need to update the graph (the animation needs to run a function and that was the easiest way i found) – Adi Portal Oct 03 '19 at 07:18

1 Answers1

0

Since markers are used and your constantly clearing and redrawing the plot, you need to cycle through the axes lines and check if it contains the current event; for the same reason, some persistent data about the annotation is required.

class ApplicationWindow(QtWidgets.QMainWindow):
    # ...
    def __init__(self):
        # ...
        self.tooltip_visible = False
        self.tooltip_coords = 0, 0
        self.tooltip_text = ''
        self.ani = FuncAnimation(self.canvas.figure, self.animate, interval=1000, blit=False)
        self.canvas.figure.canvas.mpl_connect('pick_event', ApplicationWindow.on_pick)
        self.canvas.mpl_connect("motion_notify_event", self.hover)

    def hover(self, event):
        if event.inaxes == self.canvas.ax:
            for line in self.canvas.ax.lines:
                contains, index = line.contains(event)
                if contains:
                    self.tooltip.set_text(line.get_label())
                    self.tooltip.set_x(line.get_xdata())
                    self.tooltip.set_y(line.get_ydata())
                    self.tooltip.set_visible(True)
                    self.tooltip_coords = line.get_xdata(), line.get_ydata()
                    self.tooltip_text = line.get_label()
                    break
            else:
                self.tooltip.set_visible(False)
        self.tooltip_visible = self.tooltip._visible
        # redraw the canvas to display or hide the label
        self.canvas.draw()

    def animate(self, i):
        self.canvas.ax.clear()
        self.tooltip = self.canvas.ax.annotate(self.tooltip_text, self.tooltip_coords)
        self.tooltip.set_visible(self.tooltip_visible)
        self.create_plot()

I have to warn you, though, that your implementation has some issues. The advantage of animation is to change properties of persistent objects, redrawing the whole plot at each pass is not a good solution. All markers can be created just once: you can create an internal list of markers and extend it for each canvas.ax.plot (which returns a list of markers created), then use the animation to set that data. Since you are using the blit argument, the animation function should return the new data about each object, otherwise there's no point in using it. Have a look at the animation api documentation.
Finally, I think you should youse instance methods for connections, and not class methods (since they won't let you access the instance attributes, including the canvas itself).

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • I'm definitely going to check this out, but i have a question about your last two lines - where do you think would be a good place to put the canvas? Because i found it hard to access it as you mentioned... – Adi Portal Oct 04 '19 at 06:25
  • @AdiPortal are you talking about the connections? If that's so, just use `self.method` for connections (for example, in your case: use `self.on_pick` instead of `ApplicationWindow.on_pick`, and insert `self` as the first `def on_pick()` argument. – musicamante Oct 04 '19 at 06:36
  • Everythong working very well, i tried to add to the label an arrow that points on the point (`arrowprops=dict(arrowstyle="->")`) but for points that close to the edge the label is out of the frame. Is there a way to make it stay inside? Because for some points you just can't see anything in the label – Adi Portal Oct 04 '19 at 08:00
  • I've changed my create_plot method after your advice and now i'm using `for xp, yp, c, m, l in zip(x, y, color, marker, labels): MyMplCanvas.ax.plot([xp], [yp], color=c, marker=m, markersize=5, label=l, picker=5)` what should i cange in the animation method in order to not clear and plot it over and over? – Adi Portal Oct 05 '19 at 13:37