Skip to content

[ENH] Proposal how to fix blitting in "webagg" (and potentially "ipympl") #27125

Closed
@raphaelquast

Description

@raphaelquast

As the title suggests, this is not an issue by itself but a proposal on how the current issues concerning blitting in webagg and ipympl backends might be resolved...

Since I'd like a bit of feedback on this before I implement it properly in a PR, here's a monkey-patch version of my suggestion.

This might also fix blitting in jupyter-notebooks (e.g. #19116) but would require sending "ack" messages in ipympl


In short, in the current implementation, the main problem for webagg blitting is the fact that events accumulate.

This results in extreme lags as shown in the gif below (green line should be updated with cursor position) and makes blitting in webagg not really useful:

My proposal is to use the "ack" message (sent once an image has been properly processed) to delay event handling so that events no longer accumulate and we get a nice and smooth blitting behavior.

Before I pack this properly in a PR, I'd like some feedback if this is actually a solution that seems feasible for you and if you see some immediate problems that might arise by this way of event handling.

The following code monkey-patches FigureCanvasWebAggCore and FigureManagerWebAgg to implement the proposed fix to avoid event accumulation.


🐍 code to monkey-patch webagg backend
from matplotlib.backends.backend_webagg_core import FigureCanvasWebAggCore, FigureManagerWebAgg

def handle_ack(self, event):
    # Network latency tends to decrease if traffic is flowing
    # in both directions.  Therefore, the browser sends back
    # an "ack" message after each image frame is received.
    # This could also be used as a simple sanity check in the
    # future, but for now the performance increase is enough
    # to justify it, even if the server does nothing with it.

    # count the number of received images
    self._ack_cnt += 1

def refresh_all(self):
    if self.web_sockets:
        diff = self.canvas.get_diff_image()
        if diff is not None:
            for s in self.web_sockets:
                s.send_binary(diff)

            # count the number of sent images
            self._send_cnt += 1

def handle_event(self, event):

    cnt_equal = self._ack_cnt == self.manager._send_cnt

    # always process ack and draw events
    # process other events only if "ack count" equals "send count"
    # (e.g. if we received and handled all pending images)
    if cnt_equal or event["type"] in ["ack", "draw"]:
        # immediately process all cached events
        for cache_event_type, cache_event in self._event_cache.items():
            getattr(self, 'handle_{0}'.format(cache_event_type),
                    self.handle_unknown_event)(cache_event)
        self._event_cache.clear()

        # reset counters to avoid overflows
        # (probably not necessary but you never know...)
        if cnt_equal:
            self.manager._send_cnt = 0
            self._ack_cnt = 0

        # process event
        e_type = event['type']
        handler = getattr(self, 'handle_{0}'.format(e_type),
                          self.handle_unknown_event)
    else:
        # ignore events in case we have a pending image that is on the way
        # to be processed

        # cache the latest event of each type so we can process it once we are ready
        self._event_cache[event["type"]] = event

        # a final savety precaution in case send count is lower than ack count
        # (e.g. we wait for an image but there was no image sent)
        if self.manager._send_cnt < self._ack_cnt:
            # reset counts... they seem to be incorrect
            self.manager._send_cnt = 0
            self._ack_cnt = 0
            # (maybe??) send a draw-event to force a refresh
            # self.send_event("draw")
        return

    return handler(event)

FigureCanvasWebAggCore._ack_cnt = 0
FigureCanvasWebAggCore.handle_ack = handle_ack
FigureCanvasWebAggCore.handle_event = handle_event

FigureCanvasWebAggCore._event_cache = dict()
FigureManagerWebAgg._send_cnt = 0
FigureManagerWebAgg.refresh_all = refresh_all
🐍 code to run example
import matplotlib as mpl
mpl.use("webagg")
mpl.rcParams['webagg.port']=8988
mpl.rcParams['webagg.open_in_browser']=True


import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(14, 8))
ax.plot([0, 1], [0, 1], 'r')
ln, = ax.plot([0, 1], [0, 0], 'g', animated=True)
ax.figure.canvas.draw()

fig._last_blit_bg = fig.canvas.copy_from_bbox(ax.bbox)

def on_resize(event):
    ax.figure.canvas.draw()
    fig._last_blit_bg = fig.canvas.copy_from_bbox(ax.bbox)

def on_move(event):
    ax.figure.canvas.restore_region(fig._last_blit_bg)
    ln.set_ydata([event.ydata, event.ydata])
    ax.draw_artist(ln)
    ax.figure.canvas.blit(ax.bbox)

fig.canvas.mpl_connect('motion_notify_event', on_move)
fig.canvas.mpl_connect('resize_event', on_resize)

plt.show()

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions