Problems with annotations using matplotllib v3.8/python

I migrated a project to a different environment using a newer version of matplotlib/python.
I started having troubles with annotations not working.

I have been able to get past some of the obstacles, but this one I don’t understand.

It seems that self.ax.fill_between stops the annotations from working when self.ax.fill_between comes before self.ax.scatter.

Does anyone know what is causing this issue?

Minimal working example:

import sys
import numpy as np
from matplotlib.backends.backend_qt5agg import \
    FigureCanvasQTAgg as FigureCanvas
from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QVBoxLayout,
    QWidget,
    )

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("Live Charts")
        self.statusbar = self.statusBar()
        self.setCentralWidget(GraphWidget())


class GraphWidget(FigureCanvas):

    def __init__(self):
        super().__init__()

        self.prev_ind = None
        # self.canvas = FigureCanvas()
        vertical_layout = QVBoxLayout(self)
        vertical_layout.addWidget(self)

        self.x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
        self.y = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])

        self.ax = self.figure.add_subplot()
        self.ax.scatter(self.x, self.y)
        self.ax.fill_between([2,7], 2, 7)

        self.annot = self.ax.annotate("", (self.x, self.y),
                                      annotation_clip=True,
                                      size=7, textcoords="offset points",
                                      bbox=dict(boxstyle="round", fc="w"),
                                      arrowprops=dict(arrowstyle="->"))

        self.annot.set_visible(False)
        self.figure.canvas.mpl_connect("motion_notify_event", self.hover)

        self.draw()

    def hover(self, event):
        vis = self.annot.get_visible()
        if event.inaxes == self.ax:
            self.cont, self.ind = self.ax.collections[0].contains(event)
            if (self.prev_ind is None or self.ind.values()
                    != self.prev_ind.values()):
                self.x_data = event.xdata
                self.y_data = event.ydata
                if self.cont:
                    self.update_annot(self.ind)
                    self.annot.set_visible(True)
                    self.prev_ind = self.ind
                    self.draw()
                else:
                    if vis:
                        self.annot.set_visible(False)
                        self.prev_ind = None
                        self.draw()
            else:
                pass

    def update_annot(self, ind):
  
        cluster = sum(len(v) for v in ind.values())
        self.ax.collections[0].get_offsets().tolist()

        pos_list = []
        xy_differences = []
  
        for element in range(cluster):
            pos_list.append(self.ax.collections[0].get_offsets()[ind["ind"][element]])
        for pos_element in pos_list:
  
            xy_differences.append([pos_element[0] - self.x_data,
                                   pos_element[1] - self.y_data])
        xy_totals = []
        for xy_set in xy_differences:
            xy_set = sum(abs(x) for x in xy_set)
            xy_totals.append(xy_set)
  
        index = xy_totals.index(min(xy_totals))
  
        pos = self.ax.collections[0].get_offsets()[ind["ind"][index]]
  
        self.annot.xy = pos
  
        self.annot.set_x(25)
        self.annot.set_y(2)
  
        text = "SN: {}".format("test")
  
        center_x = (0 + 9) / 2
        center_y = (0 + 9) / 2
  
        if ((self.ax.collections[0].get_offsets()[ind["ind"][index]][0])
            > center_x
            and (self.ax.collections[0].get_offsets()[ind["ind"][index]][1]
                 > center_y)):
            self.annot.set_x(-120)
            self.annot.set_y(-26)
  
        elif ((self.ax.collections[0].get_offsets()[ind["ind"][index]][0])
              < center_x
              and (self.ax.collections[0].get_offsets()[ind["ind"][index]][1]
                   > center_y)):
            self.annot.set_x(25)
            self.annot.set_y(-26)
  
        elif ((self.ax.collections[0].get_offsets()[ind["ind"][index]][0])
              < center_x
              and (self.ax.collections[0].get_offsets()[ind["ind"][index]][1]
                   < center_y)):
            self.annot.set_x(25)
            self.annot.set_y(4)
  
        elif ((self.ax.collections[0].get_offsets()[ind["ind"][index]][0])
              > center_x
              and (self.ax.collections[0].get_offsets()[ind["ind"][index]][1]
                   < center_y)):
            self.annot.set_x(-120)
            self.annot.set_y(4)
  
        self.annot.set_text("TEST")
        self.annot.get_bbox_patch().set_alpha(1)

if __name__ == '__main__':

    app = QApplication(sys.argv)
    window = MainWindow()
    window.showMaximized()
    window.activateWindow()
    sys.exit(app.exec())

fill_between returns a PolyCollection so I suspect that when you access self.ax.collections[0] you are getting hold of that rather than the PathCollection from scatter.

1 Like

Thank you, that explanation helped me figure some things out.
self.ax contains a list of children (artistlist?). The order of the children within the list depends on the order in which I place fill_between/scatter. I either need to use self.ax.collections[0], or self.ax.collections[1].

list -
[<matplotlib.collections.PolyCollection object at 0x00000278A236CED0>, <matplotlib.collections.PathCollection object at 0x00000278A236ED10>, Text(-120, -26, ‘TEST’), <matplotlib.spines.Spine object at 0x00000278A2310F10>, <matplotlib.spines.Spine object at 0x00000278A20C8990>, <matplotlib.spines.Spine object at 0x00000278A2311650>, <matplotlib.spines.Spine object at 0x00000278A2311B90>, <matplotlib.axis.XAxis object at 0x00000278A2312110>, <matplotlib.axis.YAxis object at 0x00000278A1F0ABD0>, Text(0.5, 1.0, ‘’), Text(0.0, 1.0, ‘’), Text(1.0, 1.0, ‘’), <matplotlib.patches.Rectangle object at 0x00000278A2312A50>]