Skip to content

[Bug]: .contains for some artists does not check for visibility #23875

Open
@rdgraham

Description

@rdgraham

Bug summary

I recently managed to find the source of a long-standing bug in my application that was quite tricky to find.

It turned out to be a consequence of the contains method of a line returning true even when the line was set to not visible at the time. Although from what I can see in the documentation there is nothing prohibiting this, I think almost everyone would want contains to return false for objects which have been plotted and set to invisible.

Moreover, a non-exhaustive look through the code shows that there are class-specific checks in place for visibility for some objects, but not others. Specifically, the following do not have checks:

lines.Line2D
patches.Patch
quiver.QuiverKey

But these ones do (though not explicitly documented.)

collections.Collection
text.Text
image.BboxImage

It seems like a fix might put the check for visibility in the _default_contains method of Artist. This is called by the child implementations, but as of now seems to provide only a bail-out if the event is outside the figure canvas. Although I guess maybe it was done that way if there are some specific exceptions where you would actually want contains to return true (maybe bounding boxes or more abstract things not plotted directly).

Code for reproduction

from matplotlib import pyplot as plt

class Demo:

    def __init__(self):

        fig, ax = plt.subplots()

        self.lines = []
        self.lines.append(ax.axhline(1))
        self.lines.append(ax.axhline(-1))
        self.lines[0].set_visible(False)

        self.vis_points = ax.scatter([-0.5, 0.5], [0, 0])
        self.invis_points = ax.scatter([0], [0])
        self.invis_points.set_visible(False)
        self.points = [self.vis_points, self.invis_points]

        fig.canvas.mpl_connect('button_press_event', self.clicked)

        plt.show()

    def clicked(self, event):
        at_obj = lambda x: x.contains(event)[0]

        if any(at_obj(l) for l in self.lines):
            print(f"Clicked on a line at {event.ydata:0.3f}")
        elif any(at_obj(p) for p in self.points):
            print(f"Clicked point at {event.ydata:0.3f}")
        else:
            print('Clicked elsewhere')

if __name__ == '__main__':
    demo = Demo()

Actual outcome

It is possible to click anywhere near the invisible line at y=1 and see click registered message.
However, clicking at [0,0], the location of an invisible scatter point does not show a point clicked message (expected behavior).

Expected outcome

Clicking anywhere y=1 should not generate a line click message --- just as clicking at 0,0 does not generate a point clicked message.

Additional information

No response

Operating system

Arch linux, Linux mint, Windows

Matplotlib Version

3.5.0

Matplotlib Backend

qt

Python version

3.9.9

Jupyter version

No response

Installation

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions