Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scipy/matplotlib recipe with plt.connect() has trouble in python 3 (AnnoteFinder) #5601

Closed
danielboone opened this issue Dec 2, 2015 · 1 comment

Comments

@danielboone
Copy link

Hi all,
There is an interesting recipe on the following link that I like to use on my plots:
http://scipy-cookbook.readthedocs.org/items/Matplotlib_Interactive_Plotting.html
(For easier testing attached is a file containing the program mpl_annote_finder.py.txt)

This recipe works perfectly on python 2. It makes use of pyplot.connect(). But for an obscure reason, on python 3 it works only at the first "click": The first time we click, the point label appear, but if we click again nothing occurs. This is the same for python 3.3 and 3.4, on Windows and Debian, on 32 an 64 bits architectures.

I think the script being okay. I'm wondering if the problem is due to python 3 itself or to the newer versions of matplotlib. Maybe can you find it?

Thanks,

@tacaswell
Copy link
Member

This is indeed a python 2/3 issue. In legacy python zip is enthusiastic and returned a list of the zipped values. In python 3 zip returns a generator (like itertools.izip). The first time you click the generator is exhausted and hence the loop is not run through on all subsequent events. Wrapping the zip call in list fixes the problem.

from __future__ import division

'''
Click on a datapoint and see a label of what that point is.
Click again and the label disappear

This is a recipe from:
http://scipy-cookbook.readthedocs.org/items/Matplotlib_Interactive_Plotting.html

Minor adjustments, e.g: 4 space tabs, invoke pyplot API instead of Pylab, ...
'''

import math

import matplotlib.pyplot as plt


class AnnoteFinder(object):
    """callback for matplotlib to display an annotation when points are
    clicked on.  The point which is closest to the click and within
    xtol and ytol is identified.

    Register this function like this:

    scatter(xdata, ydata)
    af = AnnoteFinder(xdata, ydata, annotes)
    connect('button_press_event', af)

    """

    def __init__(self, xdata, ydata, annotes, ax=None, xtol=None, ytol=None):
        # CHANGED THIS LINE
        self.data = list(zip(xdata, ydata, annotes))
        if xtol is None:
            xtol = ((max(xdata) - min(xdata))/float(len(xdata)))/2
        if ytol is None:
            ytol = ((max(ydata) - min(ydata))/float(len(ydata)))/2
        self.xtol = xtol
        self.ytol = ytol
        if ax is None:
            self.ax = plt.gca()
        else:
            self.ax = ax
        self.drawnAnnotations = {}
        self.links = []

    def distance(self, x1, x2, y1, y2):
        """
        return the distance between two points
        """
        return(math.sqrt((x1 - x2)**2 + (y1 - y2)**2))

    def __call__(self, event):

        if event.inaxes:

            clickX = event.xdata
            clickY = event.ydata
            if (self.ax is None) or (self.ax is event.inaxes):
                annotes = []
                print(event.xdata, event.ydata)
                for x, y, a in self.data:
                    print(x, y, a)
                    if ((clickX-self.xtol < x < clickX+self.xtol) and
                            (clickY-self.ytol < y < clickY+self.ytol)):
                        annotes.append(
                            (self.distance(x, clickX, y, clickY), x, y, a))
                if annotes:
                    annotes.sort()
                    distance, x, y, annote = annotes[0]
                    self.drawAnnote(event.inaxes, x, y, annote)
                    for l in self.links:
                        l.drawSpecificAnnote(annote)

    def drawAnnote(self, ax, x, y, annote):
        """
        Draw the annotation on the plot
        """
        if (x, y) in self.drawnAnnotations:
            markers = self.drawnAnnotations[(x, y)]
            for m in markers:
                m.set_visible(not m.get_visible())
            self.ax.figure.canvas.draw_idle()
        else:
            t = ax.text(x, y, " - %s" % (annote),)
            m = ax.scatter([x], [y], marker='d', c='r', zorder=100)
            self.drawnAnnotations[(x, y)] = (t, m)
            self.ax.figure.canvas.draw_idle()

    def drawSpecificAnnote(self, annote):
        annotesToDraw = [(x, y, a) for x, y, a in self.data if a == annote]
        for x, y, a in annotesToDraw:
            self.drawAnnote(self.ax, x, y, a)

if __name__ == '__main__':
    x = range(10)
    y = range(10)
    annotes = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'a']
    fig, ax = plt.subplots()
    ax.scatter(x, y)   # or plt.plot(x,y,'bo')
    af = AnnoteFinder(x, y, annotes, ax=ax)
    fig.canvas.mpl_connect('button_press_event', af)

    plt.show()

should work. I also made a number of style changes. I think they would greatly appreciate if you pushed these changes back up into the cookbook, see the directions at https://github.com/pv/SciPy-Cookbook/.

Closing as there is nothing to be done on the mpl side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants