Gauging interest in a AsynchronousAnimation class

Hi,

Out of my own needs I developed an animation class that is very similar to
FuncAnimation but is drawn asynchronously (not that this couldn't be done
with FuncAnimation, this is just a more direct path). Instead of trying to
explain the differences off the bat I'll give a base example usage

fig, ax = plt.subplots()
x = np.arange(0, 2*np.pi, 0.01)
line, = ax.plot(x, np.sin(x))

ani = AsyncAnimation(fig,interval=50)
fig.show() # starts the first 'draw' without entering the mpl mainloop

i = 0
while ani.update(): #update() flushes the animation's queue
    i += 1
    y = np.sin(x+i/10.0)
    ani.append_artist(line,(x,y))

This is the simplest example, where one frame is queued at a time and
`update()` blocks execution until that frame has been rendered (so this is
effectively the same as a FuncAnimation).

Aesthetically, the difference is that the animation code lives inside a
loop instead of a function call. This removes the need to have the
animation code pass variables back to the user or doing any global imports
of variables which IMHO makes the application code cleaner and more
self-contained. Personally, I find this a simpler and more intuitive way to
write interactive animation applications. (Alternatively, writing
application code inside of a class that uses FuncAnimation gives you
similar benefits of cleaner code).

The other more significant difference is that having rendering being done
asynchronously basically decouples the animation code from the application
code, which

   1. Makes it trivial to execute the graphics rendering as a separate
   process
   2. Gives the user more flexibility in their application code (you could
   imagine an application "queueing up" multiple frames at a time, allowing
   them to render in their own time).

Anyway, here is the code as it stands now (most important are the
`append_artist` and `update` functions; the rest of the code is very
similar to the FuncAnimation class):

class AsyncAnimation(Animation):
    """
    Makes an animation by reading from a user filled queue every *interval*
    milliseconds. User adds to the queue by calling *update_artists*.

    Note that data is _not_ copied before being put into the queue; if a
reference
    is passed (such as a numpy ndarray) whatever data the reference is
pointing
    to at draw time will be plotted.

    *init_func* is a function used to draw a clear frame. If not given, the
    results of drawing from the first item in the frames sequence will be
    used. This function will be called once before the first frame.

    *event_source* Default event source to trigger poll of the artist
queue. By
    default this is a timer with an *interval* millisecond timeout.
    """
    def
__init__(self,fig,init_func=None,event_source=None,interval=10,blit=True):
        self._data = deque()
        self._lag = 0
        self._queued = False

        self._fig = fig
        self._interval = interval

        self._init_func = init_func

        if event_source is None:
            event_source = fig.canvas.new_timer(interval=self._interval)

        Animation.__init__(self,fig,event_source=event_source,blit=blit)

    def new_frame_seq(self):
        return itertools.count()

    def _init_draw(self):
        if self._init_func is not None:
            self._drawn_artists = self._init_func()
            for a in self._drawn_artists:
                a.set_animated(self._blit)

    def _draw_next_frame(self, *args):
        # carry on if there's nothing to draw right now
        if self._data:
            Animation._draw_next_frame(self,*args)

    def _draw_frame(self,framedata):
        artdata = self._data.popleft()

        artists = []
        for (a,d) in artdata:
            artists.append(a)
            if d is not None: a.set_data(d)
            a.set_animated(self._blit)
        self._drawn_artists = artists

    def append_artist(self,artist,data=None):
        self._queue_artists((artist,data),)

    def extend_artists(self,artists):
        self._queue_artists(*artists)

    def _queue_artists(self,*artists):
        if len(self._data) and self._queued:
            self._data[-1] += artists
            return

        self._queued = True

        if len(self._data) > self._lag:
            warnings.warn("Artists queue is behind by %d" % len(self._data))
            self._lag = len(self._data)

        self._data.append(artists)

    def update(self,block=True):
        self._queued = False
        self._fig.canvas.flush_events()

        if block:
            while len(self._data) and self.event_source is not None:
                self._fig.canvas.flush_events()

        return self.event_source is not None

So, I've developed this code enough that it fills my needs, but it needs
some work to add in all the expected matplotlib functionality. Is something
that would be useful to people?
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/matplotlib-devel/attachments/20150919/eb64a085/attachment.html>

Sorry we haven't gotten back to you sooner, we are currently focusing on
getting the long-awaited v1.5/v2.0 releases finalized.

I will admit that I have been absolutely hating the simple animation
examples that require using globals. And the prospect of decoupling the
animation & application code is attractive. I will point out that this code
wouldn't work for all artists (particularly 3D objects), but it is a good
first step, and would help give us a kick in the pants to normalize data
setting across all artists.

Cheers!
Ben Root

···

On Sat, Sep 19, 2015 at 3:47 PM, toemoss garcia <toemossgarcia at gmail.com> wrote:

Hi,

Out of my own needs I developed an animation class that is very similar to
FuncAnimation but is drawn asynchronously (not that this couldn't be done
with FuncAnimation, this is just a more direct path). Instead of trying to
explain the differences off the bat I'll give a base example usage

fig, ax = plt.subplots()
x = np.arange(0, 2*np.pi, 0.01)
line, = ax.plot(x, np.sin(x))

ani = AsyncAnimation(fig,interval=50)
fig.show() # starts the first 'draw' without entering the mpl mainloop

i = 0
while ani.update(): #update() flushes the animation's queue
    i += 1
    y = np.sin(x+i/10.0)
    ani.append_artist(line,(x,y))

This is the simplest example, where one frame is queued at a time and
`update()` blocks execution until that frame has been rendered (so this is
effectively the same as a FuncAnimation).

Aesthetically, the difference is that the animation code lives inside a
loop instead of a function call. This removes the need to have the
animation code pass variables back to the user or doing any global imports
of variables which IMHO makes the application code cleaner and more
self-contained. Personally, I find this a simpler and more intuitive way to
write interactive animation applications. (Alternatively, writing
application code inside of a class that uses FuncAnimation gives you
similar benefits of cleaner code).

The other more significant difference is that having rendering being done
asynchronously basically decouples the animation code from the application
code, which

   1. Makes it trivial to execute the graphics rendering as a separate
   process
   2. Gives the user more flexibility in their application code (you
   could imagine an application "queueing up" multiple frames at a time,
   allowing them to render in their own time).

Anyway, here is the code as it stands now (most important are the
`append_artist` and `update` functions; the rest of the code is very
similar to the FuncAnimation class):

class AsyncAnimation(Animation):
    """
    Makes an animation by reading from a user filled queue every *interval*
    milliseconds. User adds to the queue by calling *update_artists*.

    Note that data is _not_ copied before being put into the queue; if a
reference
    is passed (such as a numpy ndarray) whatever data the reference is
pointing
    to at draw time will be plotted.

    *init_func* is a function used to draw a clear frame. If not given, the
    results of drawing from the first item in the frames sequence will be
    used. This function will be called once before the first frame.

    *event_source* Default event source to trigger poll of the artist
queue. By
    default this is a timer with an *interval* millisecond timeout.
    """
    def
__init__(self,fig,init_func=None,event_source=None,interval=10,blit=True):
        self._data = deque()
        self._lag = 0
        self._queued = False

        self._fig = fig
        self._interval = interval

        self._init_func = init_func

        if event_source is None:
            event_source = fig.canvas.new_timer(interval=self._interval)

        Animation.__init__(self,fig,event_source=event_source,blit=blit)

    def new_frame_seq(self):
        return itertools.count()

    def _init_draw(self):
        if self._init_func is not None:
            self._drawn_artists = self._init_func()
            for a in self._drawn_artists:
                a.set_animated(self._blit)

    def _draw_next_frame(self, *args):
        # carry on if there's nothing to draw right now
        if self._data:
            Animation._draw_next_frame(self,*args)

    def _draw_frame(self,framedata):
        artdata = self._data.popleft()

        artists = []
        for (a,d) in artdata:
            artists.append(a)
            if d is not None: a.set_data(d)
            a.set_animated(self._blit)
        self._drawn_artists = artists

    def append_artist(self,artist,data=None):
        self._queue_artists((artist,data),)

    def extend_artists(self,artists):
        self._queue_artists(*artists)

    def _queue_artists(self,*artists):
        if len(self._data) and self._queued:
            self._data[-1] += artists
            return

        self._queued = True

        if len(self._data) > self._lag:
            warnings.warn("Artists queue is behind by %d" %
len(self._data))
            self._lag = len(self._data)

        self._data.append(artists)

    def update(self,block=True):
        self._queued = False
        self._fig.canvas.flush_events()

        if block:
            while len(self._data) and self.event_source is not None:
                self._fig.canvas.flush_events()

        return self.event_source is not None

So, I've developed this code enough that it fills my needs, but it needs
some work to add in all the expected matplotlib functionality. Is something
that would be useful to people?

_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel at python.org
https://mail.python.org/mailman/listinfo/matplotlib-devel

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/matplotlib-devel/attachments/20151005/339ce946/attachment.html>