Search code examples
python-3.xmatplotlibmplcursors

mplcursors: show and highlight coordinates of nearby local extreme


I have code that shows the label for each point in a matplotlib scatterplot using mplcursors, similar to this example. I want to know how to, form a list of values, make a certain point stand out, as in if I have a graph of points y=-x^2. When I go near the peak, it shouldn't show 0.001, but 0 instead, without the trouble needing to find the exact mouse placement of the top. I can't solve for each point in the graph, as I don't have a specific function.


Solution

  • Supposing the points in the scatter plot are ordered, we can investigate whether an extreme in a nearby window is also an extreme in a somewhat larger window. If, so we can report that extreme with its x and y coordinates.

    The code below only shows the annotation when we're close to a local maximum or minimum. It also temporarily shows a horizontal and vertical line to indicate the exact spot. The code can be a starting point for many variations.

    import matplotlib.pyplot as plt
    import mplcursors
    import numpy as np
    
    near_window = 10 # the width of the nearby window
    far_window = 20 # the width of the far window
    
    def show_annotation(sel):
        ind = sel.target.index
        near_start_index = max(0, ind - near_window)
        y_near = y[near_start_index: min(N, ind + near_window)]
        y_far = y[max(0, ind - far_window): min(N, ind + far_window)]
        near_max = y_near.max()
        far_max = y_far.max()
        annotation_str = ''
        if near_max == far_max:
            near_argmax = y_near.argmax()
            annotation_str = f'local max:\nx:{x[near_start_index + near_argmax]:.3f}\ny:{near_max:.3f}'
            maxline = plt.axhline(near_max, color='crimson', ls=':')
            maxline_x = plt.axvline(x[near_start_index+near_argmax], color='grey', ls=':')
            sel.extras.append(maxline)
            sel.extras.append(maxline_x)
        else:
            near_min = y_near.min()
            far_min = y_far.min()
            if near_min == far_min:
                near_argmin = y_near.argmin()
                annotation_str = f'local min:\nx:{x[near_start_index+near_argmin]:.3f}\ny:{near_min:.3f}'
                minline = plt.axhline(near_min, color='limegreen', ls=':')
                minline_x = plt.axvline(x[near_start_index + near_argmin], color='grey', ls=':')
                sel.extras.append(minline)
                sel.extras.append(minline_x)
        if len(annotation_str) > 0:
            sel.annotation.set_text(annotation_str)
        else:
            sel.annotation.set_visible(False) # hide the annotation
            # sel.annotation.set_text(f'x:{sel.target[0]:.3f}\n y:{sel.target[1]:.3f}')
    
    N = 500
    x = np.linspace(0, 100, 500)
    y = np.cumsum(np.random.normal(0, 0.1, N))
    box = np.ones(20) / 20
    y = np.convolve(y, box, mode='same')
    scat = plt.scatter(x, y, s=1)
    
    cursor = mplcursors.cursor(scat, hover=True)
    cursor.connect('add', show_annotation)
    
    plt.show()
    

    example plot