0

I apologize since I have open another ticket before on a related topic. Thanks to the answers I got now I can be more specific. I have also received some solutions based on Tkinter, but I would like to solve my problems with events and loops.

The particular case I am dealing with is as follows: I have an array of arrays. I want matplotlib to plot the first element of it, allow me to press one key (with an associated event), and the program plots the second array, same behaviour, and so on.

As a quick example:

import matplotlib.pyplot as plt
import numpy as np

# Define the event
def ontype(event):
    if event.key == '1':
        print 'It is working'
        plt.clf()

# Create figure an connect the event to it
fig=plt.figure(figsize=(16,8))
plt.gcf().canvas.mpl_connect('key_press_event',ontype)

# Loop
for element in xrange(10):
    #This mimicks the "array of arrays" generating a random array in each loop
    vector = np.random.random(10)  
    plt.plot(vector)
    plt.show()

I would expect to get a first plot (the first time the loop runs), and that it is left open until I press 1. However, what I get is an figure with the ten vectors plotted, and when I press 1 the figure is cleared and it says "It is working" via terminal. I need the program to plot the first one, and move to the next element once a key has been pressed. Any hint on this? What am I doing wrong?

Thank you guys!

EDIT:

Please keep in mind that in principle, the structure of the program can not be varied, and the for loop is needed to compute different things before plotting anything. Hence, the program should go

def ontype(event):
     define event

Some stuff
elements = array of arrays
for element in elements:
    do more stuff
    plot element and "stay" in this plot untill any event key is pressed. And then, go to the next element in elements and do the same

EDIT 2:

I think I didn't explained myself properly and the kind of data might have been missunderstood. In my case, I am reading a huge table of data, and each line is a different source. Whay I am trying to plot is the information of the columns. I am a physicist, so I don't have much knowledge about stylish programming or anything. The problem is...if there is no way to do this with a for loop, could anyone explain me how to do this kind of work without it?

Álvaro
  • 1,219
  • 2
  • 12
  • 20

2 Answers2

2

This next block is what does what you want with the for loop.

def ugly_math():
    print 'you will hit this once'
    for j in range(10):
        print 'loop ', j
        # insert math here
        yield  np.random.random(10) * j

Your for loop goes into the function ugly_math, and what you want plotted is what goes after yield. see What does the "yield" keyword do in Python? . In short, yield turns a function with a loop into a generator factory.

fun = ugly_math()

is then a generator. When you call fun.next() it will run the function ugly_math until it hits the yield. It will then return the value yielded (in this example, np.random.random). The next time you call fun.next() it will pick up where it left off in the loop and run until it hits yield again. Hence it does exactly what you want.

Then borrowing heavily from Holger:

fun = ugly_math()
cid_dict = {}
# Define the event
def ontype(event):
    if event.key == '1':
        print 'It is working'
        try:
            vector = fun.next()
            plt.plot(vector)
            fig.canvas.draw()
        except StopIteration:
            plt.gcf().canvas.mpl_disconnect(cid_dict['cid'])
            del cid_dict['cid']

# Create figure an connect the event to it
fig=plt.figure(figsize=(16,8))
cid_dict['cid'] = plt.gcf().canvas.mpl_connect('key_press_event',ontype)

vector = np.random.random(10)  
plt.plot(vector)
plt.show()

The cid_dict is there so that we can remove the call-back after we have exhausted the generator.

We can wrap this all up into a class as such

class push_to_advance(object):
    def __init__(self):
        self.fig = plt.figure()
        self.ax = self.fig.gca()
        self.bound_keys = []
        self.bound_cid = {}

    def add_step_through(self, gen, key):
        key = key[0] # make a single char
        if key in self.bound_keys:
            raise RuntimeError("key %s already bound"%key)
        first_data = gen.next()
        self.ax.plot(first_data)
        self.fig.canvas.draw()
        self.bound_keys.append(key)
        def ontype(event):
            if event.key == key:
                try:
                    self.ax.plot(gen.next())
                    self.fig.canvas.draw()
                except StopIteration:
                    self.fig.canvas.mpl_disconnect(self.bound_cid[key])
                    del self.bound_cid[key]
                    self.bound_keys.remove(key)

        self.bound_cid[key] = self.fig.canvas.mpl_connect('key_press_event', ontype)

This is used as such:

 pta = push_to_advance()
 gen = ugly_math()
 pta.add_step_through(gen,'a')

Any iterable will work with a bit of finessing:

 test_array = np.arange(100).reshape(10,10)
 pta.add_step_through(test_array.__iter__(), 'b')

This amused me enough I saved it as a gist.

Community
  • 1
  • 1
tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • Well, I will surely save this for future uses, it looks great. But still not what I need: all of this should be embebded in a for loop. I will edit the question to clarify it. Thanks! – Álvaro Jan 16 '13 at 10:31
  • @Álvaro, yes, that is exactly what the `yield` does. – tacaswell Jan 16 '13 at 14:13
  • Hm...it might be the solution, I will take a look. But could it be used inside an event, like the ontype one in the example? – Álvaro Jan 16 '13 at 14:39
  • @Álvaro I am also a physicist, so that is no excuse;) I added some print statements to `ugly_math` that will hopefully make it clearer what is going on when you run it. This is the _easy_ way to do what you want (even if the logic seems inside out). – tacaswell Jan 16 '13 at 15:12
  • I think I am still not getting the idea behind all of this. As I understand it (not totally I feel...) is that yield returns one value, and that value is kept for the next iteration. In my case, I have a 2000 lines code with a main for loop, using approx 100 different variables and at the end of each run of this main for loop, it gets a figure. Also, just the "ontype" rutine requires 8 variables to work (this is a simplified version). The yield approach seems to requite to modify the whole program, which I will do if there is no other way, but... can it be done without this huge modification? – Álvaro Jan 16 '13 at 15:18
  • I am starting to see some light here! But now I have some questions: I need a lot of arguments to be passed through yield (not only an array, but paths, factors, ...). Should I put all of that inside a vector and use it to to the plotting then? And then, the "disentangling" should be done inside the event, true? – Álvaro Jan 16 '13 at 15:37
  • Working! I would marry you right now :) Now the only problem I am finding is that it takes some time (approx 8 secs) for each object to show. I measured different time intervals, and I noticed that the most time consuming step was actually making matplotlib plot the figure! Any suggestion on this? Any way to optimize it? Thanks a lot!! – Álvaro Jan 16 '13 at 17:04
  • @Álvaro I suggest starting a new question about optimizing the plotting routines. It will depend on the details of your plotting, if you can re-use any of the objects between frames, ect. – tacaswell Jan 16 '13 at 18:11
  • I am trying to implement this class in the current python/matplolib version. However I get the error with the ontype class method not found. Which would be the current design for the push_to_advance class? – Delosari May 06 '21 at 21:32
1

You do not need a loop. Redraw the new plots in the event function ontype with the command fig.canvas.draw().

import matplotlib.pyplot as plt
import numpy as np

# Define the event
def ontype(event):
    if event.key == '1':
        print 'It is working'
        vector = np.random.random(10)  
        plt.plot(vector)
        fig.canvas.draw()

# Create figure an connect the event to it
fig=plt.figure(figsize=(16,8))
plt.gcf().canvas.mpl_connect('key_press_event',ontype)

vector = np.random.random(10)  
plt.plot(vector)
plt.show()
Holger
  • 2,125
  • 2
  • 19
  • 30
  • Hm...the thing is I actually need the loop. In each iteration, I calculate the "array" I have to plot. That's probably the problem, but due to the way the data are given, I can't think of any other solution... But you example worked pretty well, maybe there is a way... – Álvaro Jan 15 '13 at 22:38
  • No, you don't need the for loop. You should calculate your data inside the 'ontype' routine. Why isn't that possible? – Holger Jan 15 '13 at 23:00
  • It might be possible, but complicated. Inside the for loop (before plotting anything), several functions and classes are used to deal and modify the data, and depending on some conditions they are treated in a way or another. The data aren't homogeneus, and it will also imply passing like twenty variables (or more) to the ontype routin So... don't know, maybe I could try to do all of that inside the ontype routine, but it feels weird that it is not possible to simply show one plot, press one button and get the for loop going. – Álvaro Jan 15 '13 at 23:07
  • @Álvaro Use Closures and generators. – tacaswell Jan 16 '13 at 05:28