1

This class plots a curve in Matplotlib. The user mouse input section changes the set_data() for several x,y coordinates. The P and Q are resetting properly, it seems. However, when the R is not set with calculations using those same methods (set_data() or set_x() or set_y()), then this results in the error:

TypeError: unsupported operand type(s) for ** or pow(): 'NoneType' and 'int'

When the R calculations are left in this results in the error:

AttributeError: 'list' object has no attribute 'set_xdata'

The whole class (it's a little big but the methods are interdependent and I don't want to leave out something that could be relevant here):

from mpl_toolkits.axes_grid.axislines import SubplotZero
import numpy as np
import matplotlib.pyplot as plt
from math import sqrt



class ECC(object):

    def __init__(self,a,b,px,qx,qy):
        """
        initialize input variables
        """
        self.a = a
        self.b = b
        self.pxlam = px
        self.qxlam = qx
        self.invertQy = qy
        self.fig = plt.figure(1)
        self.ax = SubplotZero(self.fig, 111)
        self.xr = 0
        self.yr = 0



    def onclick(self, event):
        x = event.xdata
        if event.button == 1:
            self.pxlam = x
        if event.button == 3:
            self.qxlam = x

        pylam = self.ecclambda(self.pxlam,self.a,self.b)  # calculate P from pxlam
        qylam = self.ecclambda(self.qxlam,self.a,self.b)  # calculate Q from qxlam
        if self.invertQy == 1:  qylam = -qylam # optional, inverts qy to negative on the plot
        plt.plot([self.pxlam,self.qxlam], [pylam,qylam], color = "c", linewidth=1)
        self.p = plt.plot([self.pxlam], [pylam], "mo")[0]
        self.q = plt.plot([self.qxlam], [qylam], "mo")[0]
        self.pt = plt.text(self.pxlam-0.25,pylam+0.5, '$P$')
        self.qt = plt.text(self.qxlam-0.25,self.qxlam+0.5, '$Q$')

        self.xr,self.yr = self.dataToPlotR(pylam,qylam)
        plt.plot([self.xr],[self.yr],"mo")
        plt.plot([self.xr],[-1*(self.yr)],"co")
        self.rxdata = [self.qxlam,self.xr]; self.rydata = [qylam,self.yr]
        self.r, = plt.plot(self.rxdata, self.rydata, color = "c", linewidth=1)
        #plt.plot([xr,xr], [yr,-yr], "x--")
        self.plotR(qylam)
        plt.text(self.xr+0.25,self.yr, '$-R$'); plt.text(self.xr+0.25,-1*(self.yr), '$R$')
        plt.text(-9,6,' P: (%s ,%s) \n Q: (%s ,%s) \n R: (%s ,%s) \n a: %s \n b: %s '
                %(self.pxlam,pylam,self.qxlam,qylam,self.xr,-1*(self.yr),self.a,self.b),
                fontsize=10, color = 'blue',bbox=dict(facecolor='tan', alpha=0.5))

        self.update()

    def update(self):
        pylam = self.ecclambda(self.pxlam,self.a,self.b)  # calculate P from pxlam
        qylam = self.ecclambda(self.qxlam,self.a,self.b)  # calculate Q from qxlam
        self.p.set_data([self.pxlam], [pylam])
        self.q.set_data([self.qxlam], [qylam])
        self.pt.set_x(self.pxlam-0.25)
        self.pt.set_y(pylam+0.5)
        self.qt.set_x(self.qxlam-0.25)
        self.qt.set_y(qylam+0.5)
        self.xr,self.yr = self.dataToPlotR(pylam,qylam)
        #self.rxdata.set_xdata([self.qxlam,self.xr])  # R calculations
        #self.rydata.set_ydata([qylam,self.yr])  # R calculations
        plt.gcf().canvas.draw()

        #self.plotR(self.xr,self.yr,qylam)

The lines of code that I'm referring to above, as to whether the R methods are kept in or left out, are 2 commented out in the method update() and are commented afterward with # R calculation. I'm teaching myself Matplotlib right now, so I'm sure a junior programmer can see my obvious error in a very short time, but I've been at it for some time and getting nowhere fast. The main thing I want to do here is just get the lines and points to be redrawn after every click without any of the previously set points to remain on the graph. Similarly with the textbox on the upper left of the graph, the values should be reset there after every click and not rewrite themselves over each previous string of text.

EDIT:

I have tried cla() and clf() and they don't appear to work in this case. In fact, they may not even be necessary at any point in this program as set_data() methods I used should be enough to redraw based on the new data from every click. To prove that, just uncomment the entire plotGraph() method in my class and comment out the same code in update() and you'll see that the points P and Q will be set new after ever click. The real problem is the R point, the lines, and the text box on the upper left.

stackuser
  • 869
  • 16
  • 34
  • Also, you should post the full back traces as they tell you which line the issue is on. – tacaswell Nov 29 '13 at 21:47
  • 1
    @stackuser I do agree with @tcaswell: you could have restricted your code sample to the minimum: `__init__` and `onClick` would have been enough to decribe the fact that the problem is limited to update/remove of some objects. Just publishing something like 'display of a box and line where user clicked' would have brought all mechanisms you need to handle. Moreover, 'dropping by' is exactly what you expect from helpers, so please accept criticism. – Joël Nov 29 '13 at 22:14

2 Answers2

4

I've simplified the problem to its minimum, and by searching set_xdata on SO and following the link provided by tcaswell, I found this subject, that is really clear.

Here's the demo code, written in 5 minutes:

import matplotlib.pyplot as plt

class OnClickTest(object):
    def __init__(self):
        self.fig = plt.figure()

        plt.plot([0, 1, 2], [0, 4, 3])
        self.line, self.text, self.prev_click = None, None, None
        self.fig.canvas.mpl_connect('button_press_event', self.onClick)

        plt.show()

    def onClick(self, event):
        x, y = event.xdata, event.ydata
        if self.line is None:
            # creating the object
            self.line, = plt.plot([0, x], [0, y])
            self.text = plt.text(x, y, "My click")
            self.prev_click = (x, y)
        else:
            # updating the object
            self.line.set_xdata([self.prev_click[0], x])
            self.line.set_ydata([self.prev_click[1], y])
            self.text.set_position((x, y))
            self.prev_click = (x, y)
        self.fig.canvas.draw()

o = OnClickTest()
Community
  • 1
  • 1
Joël
  • 2,723
  • 18
  • 36
2

Well, at first self.rxdata and self.rydata are lists, which have no set_xdata method, hence the error. Maybe you wanted to do something like self.my_plot.set_xdata(...)?

Anyway, there's a much clever other way to do this: matplotlib is object-oriented, meaning that it handles graph stuffs as objects; and as you can add things, you can also remove some of them by calling their methods, but for this you need their reference.

So, in your case, you just have to keep the references to the objects you dant to add/remove:

  • In __init_() definition, simply add a reference tracker:

    def __init__(...):
        (...)
        self.text = None
    
  • And use this reference in onClick() definition:

    if self.text is not None:     # if text has already been drawn before,
        self.text.remove()        # simply remove it
    self.text = plt.text(-9, 6,   # and re-create it, keeping the reference
                         ' P: (%s ,%s) \n Q: (%s ,%s) \n R: (%s ,%s) \n a: %s \n b: %s '
                         (...))
    

With this only addition, and keeping the two lines returning error commented, the text box is refreshed at every click on the graph.

I think you get the point, and are able to reproduce this to every object you want to remove and recreate; there is no interest in re-drawing the ellipsis plot.


Edit after comment

Ok, remove exits for all objects, but in fact, plt.plot return a list with one element. So, a solution is simply to create a list of all objects that will be refreshed, and to call this remove method for everyone of them:

  • in __init__():

    def __init__(...):
        (...)
        self._tracker = []
    
  • in plotR(), we have to return the references:

    def plotR(self,qylam):
        r1, = plt.plot([self.qxlam, self.xr], [qylam, self.yr], color = "c", linewidth=1)
        r2, = plt.plot([self.xr, self.xr], [self.yr, -1*(self.yr)], "x--")
        return r1, r2
    
  • in onClick(), I propose the following code:

    def onclick(self, event):
         # removing elements that will be regenerated
        for element in self._tracker:      #print "trying to remove", element
            element.remove()
        # reinitializing the tracker
        self._tracker = []
        (...)
    
        _e1 = plt.plot([self.pxlam,self.qxlam], [pylam,qylam], 
                       color = "c", linewidth=1)[0]
        (...)
        _e2 = plt.plot([self.xr],[self.yr],"mo")[0]
        _e3 = plt.plot([self.xr],[-1*(self.yr)],"co")[0]
        (...)
        _e4, _e5 = self.plotR(qylam)
        _e6 = plt.text(self.xr+0.25,self.yr, '$-R$'); plt.text(self.xr+0.25,-1*(self.yr), '$R$')
        _e7 = plt.text(-9,6,' P: (%s ,%s) \n Q: (%s ,%s) \n R: (%s ,%s) \n a: %s \n b: %s '
                       %(self.pxlam,pylam,self.qxlam,qylam,self.xr,-1*(self.yr),self.a,self.b),
                       fontsize=10, color = 'blue',bbox=dict(facecolor='tan', alpha=0.5))
        (...)
    
        # adding in the tracker
        self._tracker.extend([self.p, self.q, self.pt, self.qt, self.r,
                              _e1, _e2, _e3, _e4, _e5, _e6, _e7])
    
        self.update()
    

Does this solve the problem?

Note: another solution could be to change parameters (position, data) of the objects, {edit} as shown by tcaswell's comment below and implemented in my other answer to the question.

Community
  • 1
  • 1
Joël
  • 2,723
  • 18
  • 36
  • Ok, in fact I randomly choose the `plt.text`, which appears to be the simplest example; for other objects, `remove` exists but is harder to use (see [this long post](http://stackoverflow.com/a/13575495/736151).) I will try to update my answer accordingly. – Joël Nov 29 '13 at 21:30
  • @Joël The point of that post is 'keep track of the returned objects' and makes it far more complicated than it needs to be. `ln, = ax.plot(...)` and then `ln.remove(); del ln` should work just fine. In new versions of mpl (the OP is using 0.99) – tacaswell Nov 29 '13 at 22:02
  • I just updated my answer in the direction I took initially. But, @tcaswell, with `ln, = (...)`, aren't you just exactly 'keeping track of the objects'? I don't get the difference. – Joël Nov 29 '13 at 22:06
  • Yes, but the post makes a big point of comparing the list returned from `plot` to the `ax.lines` attribute which, other than being named the same are, unrelated. It's not _wrong_ just needlessly confusing. It is addressing a bug that existed in very old versions of mpl – tacaswell Nov 29 '13 at 22:13
  • 1
    `Text` objects can have all of their attributes changed: http://matplotlib.org/api/artist_api.html#matplotlib.text.Text – tacaswell Nov 29 '13 at 22:16
  • @tcaswell Right, I put a link to another question, where those aspects are discussed (but I'm not working on this in my question, as garbage collection seems not to be the concern here). Concerning `text` object attributes updates, thanks for the link; I must admit that, as first written in my answer and now that OP has a way to achieve their goal, I let the OP have a deeper look to the documentation for cleverer ways ;) – Joël Nov 29 '13 at 22:20
  • No I'm afraid that doesn't answer it. In fact, it has no movement at all now, and I've been tinkering with it to try to get it to work, but it's not happening. I'll take all your suggestions into consideration. – stackuser Nov 29 '13 at 23:02
  • Well, I'm writing and testing this with Python 2.7 and Matplotlib 1.2.1; according to tcaswell, @stackuser you are using version 0.99, is that correct? Are you able to upgrade to a more recent version? Otherwise, did you try to do the `ln.remove(); del ln` solution? – Joël Nov 29 '13 at 23:21
  • No that's wrong. I'm using Python 2.7, Matplotlib 1.3.1 I just checked the import, it appears to be the latest version. Also, it seems like the `set_xdata()` type solution is better. It seems like you can keep making and deleting lines, but for garbage collection, etc, this is like a band-aid whereas I'd like to solve this with some strong code. Do you think that's not possible? That's why I mentioned that specifically in my post. Would you say that I'm mistaken that 1 way is better than the other in this case? – stackuser Nov 29 '13 at 23:36
  • Ok, I was misdirected by your attempt to use `cla()`, `clf()`, that removes everything on the axis/figure; my proposal is in this direction, first removing objects before re-creation. In terms of garbage-collection, I'm not really concerned as we talk about small objects, but I agree it maybe an issue on a general point of view. I will propose another answer. – Joël Nov 30 '13 at 10:21
  • @stackuser Maybe one of the two solutions I proposed has answered your problem? In this case, would you please accept one of them? – Joël Jan 30 '14 at 13:24
  • Hi I ended up solving this on my own, your solutions weren't really close enough so I was waiting to accept a much closer answer. Not sure how long I'm supposed to wait before just answering it myself. Of course I'd rather give someone the points for the right answer, but there's something distasteful about being more concerned with getting points than with answering questions and sharing knowledge. – stackuser Jan 31 '14 at 17:43
  • If you have a solution, why would you not document it, so that your knowledge is now shared? Then, in which way did I not answer your question, could you please elaborate? Last point, I'm not sure to correctly understand your last sentence: who is concerned about points, and not about answering question and sharing knowledge? – Joël Feb 03 '14 at 14:24