Matplotlib EllipseSelector – how to get the path?

Below is a basic example of the matplotlib widget EllipseSelector. As the name suggests, this widget is used to select data by drawing an ellipse onto an axis.
To be exact, the user can draw and modify an ellipse by clicking and dragging on the axis. Every time the mouse button is released a callback function (example: onselect) is called.
Here is the example:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import EllipseSelector

class EllipseExample:

    def __init__(self):

        # creating data points
        self.X, self.Y = (0, 1, 2), (0, -1, -2)
        self.XY = np.asarray((self.X, self.Y)).T

        # plotting
        self.fig, self.ax = plt.subplots()
        self.ax.scatter(self.X, self.Y) # just for visualization

        # creating the EllipseSelector
        self.es = EllipseSelector(self.ax, self.onselect,
                                  drawtype='box', interactive=True)

        # bool array about selection status of XY rows.
        self.selection_bool = None # e.g. (False, True, False)
        
        plt.show()

    # selector callback method
    def onselect(self, eclick, erelease):
        print('click: (%f, %f)' % (eclick.xdata, eclick.ydata))
        print('release  : (%f, %f)' % (erelease.xdata, erelease.ydata))
        # how to get the path of the selector's ellipse?
        # path = self.es.??? <--- no clue how to get there
        # self.selection_bool = path.contains_points(self.XY)
        # print('selection:n', self.selection_bool)

example = EllipseExample()

I’ve used other matplotlib selection widgets (PolygonSelector, RectangleSelector, LassoSelector). These all in a way return selection vertices corresponding to the selection shape, which can be used to directly filter data (e.g. RectangleSelector gives x0, x1, y0, y1 coordinates of the rectangle extents) or to create a path and check via path.contains_points if data is within the selection.
Basically I’m asking:
How can I make use of the EllipseSelector for not just drawing and ellipse but for the selector part? How to get the path of the drawn ellipse, so I can check my data via path.contains_points, as is suggested in the comments in above example.

Answer

There seems to be no direct way of checking whether points are contained in the selector via .contains_points(). The simplest way I could find is to create an ellipse patch from the EllipseSelector’s properties. These properties are inherited from the RectangleSelector btw.
By passing the center, width, and height of the selection to matplotlib.patches.Ellipse, we get an ellipse patch upon which we can call the method contains_points(). This method returns a bool ndarray, each element corresponding to a data point (True: selection contains point, False: selection doesn’t contain point).
Said bool array can be used to e.g. filter a pandas dataframe.
Caution: Under no circumstances add this patch to an axis (i.e. don’t plot this patch) as its coordinates will be transformed and you no longer can check your original data without a transformation step.
Here is a step by step beginner friendly example with verbose code commenting:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import EllipseSelector
from matplotlib.patches import Ellipse

class EllipseSelectorExample:

    def __init__(self):

        # creating data points as numpy arrays
        self.X = np.asarray((0, 1, 2, 3, 4, 5, 6))
        self.Y = np.asarray((0, 0, 0, 0, 0, 0, 0))
        
        # plotting
        self.fig, self.ax = plt.subplots()
        self.ax.set_xlim(-1, 7), self.ax.set_ylim(-3, 3)
        self.ax.grid(True)
        self.ax.scatter(self.X, self.Y)

        # creating the EllipseSelector and connecting it to onselect
        self.es = EllipseSelector(self.ax, self.onselect,
                                  drawtype='box', interactive=True)
        plt.show()

    # selector callback method
    def onselect(self, eclick, erelease):

        # 1. Collect ellipse parameters (center, width, height)

        # getting the center property of the drawn ellipse
        cx, cy = self.es.center # tuple of floats: (x, y)

        # calculating the width and height
        # self.es.extents returns tuple of floats: (xmin, xmax, ymin, ymax)
        xmin, xmax, ymin, ymax = self.es.extents
        width = xmax - xmin
        height = ymax - ymin
        print(f'center=({cx:.2f},{cy:.2f}), '
              f'width={width:.2f}, height={height:.2f}')

        # 2. Create an ellipse patch
        # CAUTION: DO NOT PLOT (==add this patch to ax), as the coordinates will
        # be transformed and you will not be able to directly check your data
        # points.
        ellipse = Ellipse((cx,cy), width, height)

        # 3. Check which points are contained in the ellipse by directly calling
        # contains_points on the ellipse.
        # contains_points wants input like ( (x0,y0), (x1,y1), ... )

        # X=x0,x1,... Y=y0,y1,...  ->  [ [x0,y0], [x1,y1], [x2,y2], ... ]
        XY = np.asarray((self.X, self.Y)).T

        # calling contains_plot and returning our filter ndarray
        filter_array = ellipse.contains_points(XY)

        # 4. Apply filter to your data (optional)
        X_filtered = self.X[filter_array]
        Y_filtered = self.Y[filter_array]

        # results:
        print(f'n'
              f'original data:nX={self.X}nY={self.Y}n'
              f'filter_array={filter_array}n'
              f'resulting data:nX={X_filtered}nY={Y_filtered}')

example = EllipseSelectorExample()

Here is a short version of above example, checking the points is only 3 lines of code:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import EllipseSelector
from matplotlib.patches import Ellipse

class EllipseSelectorExample:

    def __init__(self):
        self.MYDATA = np.array([[0, 1, 2, 3, 4, 5, 6],
                                [0, 0, 0, 0, 0, 0, 0]])
        self.fig, self.ax = plt.subplots()
        self.ax.set_xlim(-1, 7), self.ax.set_ylim(-3, 3), self.ax.grid(True)
        self.ax.scatter(self.MYDATA[0], self.MYDATA[1])
        self.es = EllipseSelector(self.ax, self.onselect,
                                  drawtype='box', interactive=True)
        plt.show()

    # selector callback method
    def onselect(self, eclick, erelease):
        ext = self.es.extents
        ellipse = Ellipse(self.es.center, ext[1]-ext[0], ext[3]-ext[2])
        # result:
        print(ellipse.contains_points(self.MYDATA.T))

example = EllipseSelectorExample()