Skip to content

The response of mouse zoom & pan is slow with Qt4Agg backend. #2559

Closed

Description

@zhangruoyu

Consider following simple program:

import matplotlib
matplotlib.use('Qt4Agg')
import pylab as pl
pl.plot(pl.rand(1000))
pl.show()

The response of mouse action is very slow.

I found that this can be fixed by changing the self.update() line
in backend_qt4agg.py to self.repaint():

def draw( self ):
    FigureCanvasAgg.draw(self)
    #self.update()
    self.repaint()

Activity

  1. Tillsten commented on Oct 30, 2013

    @Tillsten
    Contributor

    Can Confirm that matplotlib is much more responsive with the change. I always taught
    matplotlib is really slow without blitting...

  2. jason-s commented on Jan 23, 2014

    @jason-s

    Until this is released, is there a workaround in the meantime, e.g. subclassing FigureCanvasQTAgg?

    I'm not that familiar with the nuances of trying to do so. I'm using an explicit FigureCanvasQTAgg with other UI elements:

    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    

    so if subclassing would work I can definitely do it.

  3. zhangruoyu commented on Jan 24, 2014

    @zhangruoyu
    Author

    Use the patch_qt4agg() function:

    import matplotlib
    matplotlib.use("qt4agg")
    
    def patch_qt4agg():
        import matplotlib.backends.backend_qt4agg as backend
        code = """
    def draw( self ):
        FigureCanvasAgg.draw(self)
        self.repaint()
    FigureCanvasQTAgg.draw = draw    
    """
        exec(code, backend.__dict__)
    
    patch_qt4agg()
    
    import pylab as pl
    pl.plot(pl.rand(100))
    pl.show()    
    
  4. tacaswell commented on Feb 21, 2014

    @tacaswell
    Member

    @zhangruoyu @jason-s @t What OS/qt version are you using? pyqt or pysides? I use qt4agg + pysides for my daily work and have never noticed any behaviour I would call slow and the example program is snappy on my system.

    The Qt docs state that using update rather than repaint is the proper usage. I am hesitant to go against the recommended usage without a very good reason.

    http://qt-project.org/doc/qt-4.8/qwidget.html#update
    http://qt-project.org/doc/qt-4.8/qwidget.html#repaint

  5. jason-s commented on Feb 21, 2014

    @jason-s

    Windows 7. PySide. Anaconda.
    I often plot 50,000 - 200,000 points on four or five linked graphs (x-axes linked together), so maybe you never see slow behavior the way I do.

    If I get time, I will put together an example.

  6. zhangruoyu commented on Feb 21, 2014

    @zhangruoyu
    Author

    I am using windows 7, PyQt4.9.6, WinPython
    @tacaswell , have you tried the code I posted above, comment & uncomment the line patch_qt4agg(), and try mouse zoom & pan by dragging with left or right button.

    I think because update() doesn't repaint immediately, it makes the response of mouse action slow.

  7. tacaswell commented on Feb 21, 2014

    @tacaswell
    Member

    @zhangruoyu What do you mean by 'slow'? I can not reproduce this at all.

    If I plot 500k points (itplt.plot(np.linspace(0, 1, 500000), np.random.rand(500000), 'o')) is slow to respond while panning with either code. In both cases the zoom rectangle updates quickly. If I zoom in to a reasonable number of points on the screen than pan is responsive again in both cases (which makes me thing the bottle neck is in FigureCanvasAgg.draw(self).

    I suspect that the problem is windows (I am using linux)

    (edit: removed double sentence)

  8. Tillsten commented on Feb 22, 2014

    @Tillsten
    Contributor

    Windows user here, the response is sluggish while panning even for a simple [1, 2, 3] plot.

  9. mdboom commented on Feb 24, 2014

    @mdboom
    Member

    The difference between update and repaint is that update schedules a repaint for the next time the system is idle. Usually this is what you want to do, since you may move the mouse many times between each "frame", and you don't want to have to draw all of the intermediate frames -- in principle repainting with each mouse move should be slower. However, perhaps the scheduling of events is slower/behaves differently on Windows.

    However, I can't detect a difference on Linux -- so I would argue if changing to repaint has no detectable change on Linux and Mac, but improves things considerably on Windows, than we make the change.

  10. WeatherGod commented on Feb 24, 2014

    @WeatherGod
    Member

    Just a voice possibly against this idea... what if you have a very large
    image to display? The computer is already taking up a lot of cycles to
    handle the redraws on zoom and pan. To then force it to actually paint all
    those frames would only add to the burden. I would be curious how the two
    options degrade performance-wise at the upper end of displaying data. I
    regularly call imshow() on 7000x3000 images (1km resolution CONUS data),
    and then zoom in and pan around to the areas I want to view.

    On Mon, Feb 24, 2014 at 11:23 AM, Michael Droettboom <
    notifications@github.com> wrote:

    The difference between update and repaint is that update schedules a
    repaint for the next time the system is idle. Usually this is what you
    want to do, since you may move the mouse many times between each "frame",
    and you don't want to have to draw all of the intermediate frames -- in
    principle repainting with each mouse move should be slower. However,
    perhaps the scheduling of events is slower/behaves differently on Windows.

    However, I can't detect a difference on Linux -- so I would argue if
    changing to repaint has no detectable change on Linux and Mac, but improves
    things considerably on Windows, than we make the change.

    Reply to this email directly or view it on GitHubhttps://github.com//issues/2559#issuecomment-35903750
    .

  11. tacaswell commented on Feb 24, 2014

    @tacaswell
    Member

    My plan for dealing with this was to add an instantiation time switch:

    if on_windows():
        self._priv_redraw = self.repaint
    else:
        self._priv_redraw = self.update
    

    and then have draw call the private function.

    If the canvas.draw ever gets called in a repaint of something else (which given that we can be embedded is something we can't control) using repaint can lead to an infinite loop and I don't think we should expose the linux/mac users to that risk.

  12. mdboom commented on Feb 24, 2014

    @mdboom
    Member

    @WeatherGod: The actually drawing of the frame happens always, regardless, in order to support the animation model

    def draw( self ):
        FigureCanvasAgg.draw(self)
        #self.update()
        self.repaint()
    

    It is only the copying of the buffer to the window/screen that would make any difference here.

  13. mdboom commented on Feb 24, 2014

    @mdboom
    Member

    @tacaswell: That's a good point about the infinite loop. I suppose we have to guard against that somehow...

  14. efiring commented on Feb 24, 2014

    @efiring
    Member

    I would like to reinforce @WeatherGod's point; I am strongly opposed to putting in code that is contrary to the basic design of the gui toolkit to handle this Windows problem. A Windows conditional as suggested by @tacaswell would be reasonable to try. It should include a comment explaining the rationale.

  15. tacaswell commented on Feb 26, 2014

    @tacaswell
    Member

    @zhangruoyu @Tillsten @jason-s Please see #2844. I think this should address the issue.

  16. modified the milestones: v1.4.0, v1.3.x on Apr 17, 2014
  17. tacaswell commented on Apr 21, 2014

    @tacaswell
    Member

    Closing as #2844 should have fixed this.

  18. eelcovv commented on Mar 5, 2015

    @eelcovv

    The patch_qt4agg solution of zhangruoyu works for me. Thanks!

  19. tacaswell commented on Mar 5, 2015

    @tacaswell
    Member

    @eelcovv Does the issue persist in 1.4.3 with out the patch?

  20. eelcovv commented on Mar 6, 2015

    @eelcovv

    Actually, I had a slightly different situation. I have a matplotlib contourf figure in a pyQt window which I continuously want to update (playing a movie). One update works, however, when I looped over time the canvas was not updated but it seemed that all changes were hold until the last time step, and then only the last frame would show up.

    I used matplotlib 1.4.3 with python 2.7.9 and pyqt 4.10.4

    For the canvas I created a manual widget in qt-designer which I promoted to a mplCanvas class. something like

    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas 
    import matplotlib.pyplot as plt
    
    class MplCanvas(FigureCanvas):
        def __init__(self):
    
    .... so me lines here
         self.figure, self.axes = plt.subplots(nrows=1, ncols=2, subplot_kw=dict(projection='polar'), figsize=(13, 6))
          self.contour_plots = []  
    
          for index, ax in enumerate(self.axes):
                 self.contour_plots.append(
                    ax.contourf(np.zeros([1,1]),np.zeros([1,1]),np.zeros([1,1])))
    
    class mplWidget(QtGui.QWidget):
        def __init__(self, parent = None):
            QtGui.QWidget.__init__(self, parent)
            self.mplcanvas = MplCanvas()
            self.vbl = QtGui.QVBoxLayout()
            self.vbl.addWidget(self.mplcanvas)
            self.setLayout(self.vbl)
    
        def updatePlots(self,radar):
             """
             Here the radar plot is defined.
             :param radar:
             :return:
            """
    
             # this line force the canvas to be redrawn after leaving the routine
             patch_qt4agg()
    
             for index, ax in enumerate(self.mplcanvas.axes):
                ax.clear()
             for index, ax in enumerate(self.mplcanvas.axes):
                ax.hold(True)
                 tmpcont.append(ax.contourf( data_of_plot[index]))
    
                  ax.hold(False)
    
            # loop over the axes of the plot and create plot
            for index,contourplot in enumerate(self.mplcanvas.contour_plots):
                contourplot.collections=[]
                for coll in tmpcont[index].collections:
                    contourplot.collections.append(coll)
    
    
    ... some more stuff
    
            self.mplcanvas.figure.canvas.draw()
            self.mplcanvas.repaint()

    Those last two line did not cause an update of the screen when playing a movie. With your patch patch_qt4agg() it works. sorry, bit more complicated example because of the qt implementation. In the update routine I need to clear the canvas from the old plot and then push the new plot in the the collections (by making a temporary plot). Or is there something else I did wrong which could prevent from using your patch ? Anyway, it works now, so thanks for that. But any hints for improvement appreciated!

    Eelco

  21. tacaswell commented on Mar 6, 2015

    @tacaswell
    Member

    It is rather hard to follow your example. I don't really understand what you are doing with temporary plots and moving artists between axes.

    I would not use pyplot if you are doing embedding.

    You also probably do not want to monkey patch the class every time you call update.

    I suspect you want to be doing something like

    class MyMplCanvas(FigureCanvas):
        """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""
        def __init__(self, parent=None, width=5, height=4, dpi=100, ncols=2):
            fig = Figure(figsize=(width, height), dpi=dpi)
    
            FigureCanvas.__init__(self, fig)
            gs = gridspec.GridSpec(1, ncols)
            self._axes_contours = []
            for n in range(ncols):
                ax = fig.add_subplot(gs[:, n], projection='polar')
                self._axes_contours.appond(dict('ax': ax, 'contour': None))
    
        def update_plots(self):
    
            for index, ax_contour in enumerate(self._axes_contours):
                if ax_contour['contour']:
                    ax_contour['contour'].remove()
                    ax_contour['contour'] = None
                ax_contour['countour'] = ax_contour['ax'].contourf(data[index])
    
            self.draw()
  22. tacaswell commented on Mar 6, 2015

    @tacaswell
    Member

    And a related issue you are probably having is that you are not giving the gui event loop a chance to re-paint it's self. Have a look at how the animation module code works.

  23. eelcovv commented on Mar 9, 2015

    @eelcovv

    Hi Tacaswell,
    I like you comments and example and I tried to apply it to my case as it looks cleaner this way. It only does not work. First of all, ax_contour['contour'] does not have a .remove() attribute. The items from the collection list do, so I was assuming you meant that.
    Also you have combinded the class MplCanvas(FigureCanvas) and class mplWidget(QtGui.QWidget). However, I have created my canvas with qt-desinger by adding a manual widget and promoting it. The interface of the mplWiget is given by qt-desinger (in the generate .py user interface) and does not allow me to pass a argument. By splitting it I can add my own argument to the mplCanvas class, that is why I keep it separate.

    Anyway, I think that is not relevant. The problem I have now is that althoug you example (slightly modified) works, however, the old contour plot is not deleted. I can see that because it is a part of a pie which rotates and every new update of the rotation angle to old contour is not deleted, although I have looped over the collection and called the remove button.

    Here my example. Do you have any idea why the old contours do not get removed?

    import Modules.CoordinateTransforms as acf
    
    # -*- coding: utf-8 -*-
    """
    Created on Sat Feb  7 21:25:39 2015
    
    @author: eelco
    """
    
    from PyQt4 import QtGui
    
    import numpy as np
    from matplotlib.figure import Figure
    from matplotlib import cm
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    import matplotlib.gridspec as gridspec
    
    import logging
    logging.root.handlers = []
    logging.basicConfig(format='[%(levelname)-7s] %(message)s', level=logging.DEBUG)
    
    def patch_qt4agg():
        import matplotlib.backends.backend_qt4agg as backend
        code = """
    def draw( self ):
        FigureCanvasAgg.draw(self)
        self.repaint()
    FigureCanvasQTAgg.draw = draw
    """
        exec(code, backend.__dict__)
    
    
    
    
    class MplCanvas(FigureCanvas):
        """
        Create the canvas containing 2 sub plots with the amplitude and phase
        """
        def __init__(self):
    
            ncols=2
            plotlabels = ["Wave field", "Radar scan"]
            self.plot_type="Wave Field"
    
            self.figure=Figure(figsize=(13, 10),dpi=100)
            gs=gridspec.GridSpec(1,ncols)
            # create some extra space
            gs.update(left=0.15,right=.85,bottom=0.3, wspace=0.25)
    
            self._axes_contours = []
            self._axes_lines = []
            self._axes_point = []
    
            # add the empty sub figures
            for n in range(ncols):
                ax=self.figure.add_subplot(gs[:,n],projection='polar')
                ax.set_title(plotlabels[n], y=1.08)
                ax.set_theta_zero_location("N")
                ax.set_theta_direction(-1)
                ax.set_ylim(0, 1000)
                ax.set_yticks(np.linspace(0, 1000, 4, endpoint=True))
                self._axes_contours.append({'ax': ax,'contour': None,'points': None,'lines': None})
    
            # add two color bars
            self._axes_cbars=[]
            self._axes_cbars.append(self.figure.add_axes([.02, 0.35, 0.02, 0.5]))
            self._axes_cbars.append(self.figure.add_axes([.92, 0.35, 0.02, 0.5]))
    
            FigureCanvas.__init__(self, self.figure)
            FigureCanvas.setSizePolicy(self,
            QtGui.QSizePolicy.Expanding,
            QtGui.QSizePolicy.Expanding)
            FigureCanvas.updateGeometry(self)
    
    class mplWidget(QtGui.QWidget):
        """
        Widget for the matplotlib canvas and some routines to update the data
        and the x and y ranges. There as 2 subplots
        """
        def __init__(self, parent = None):
            QtGui.QWidget.__init__(self, parent)
            self.mplcanvas = MplCanvas()
            self.vbl = QtGui.QVBoxLayout()
            self.vbl.addWidget(self.mplcanvas)
            self.setLayout(self.vbl)
    
        # @profile
        def updatePlots(self,radar):
            """
            Here the radar plot is defined.
            :param radar:
            :return:
            """
    
            # this line force the canvas to be redrawn after leaving the routine
            #patch_qt4agg()
    
            logging.debug("Updating the plots")
    
            radiigrid=[radar.polar_meshgrid[1]]
            radiigrid.append(radar.scanned_area_rt_grid[1])
            anglegrid=[radar.polar_meshgrid[0]]
            anglegrid.append(radar.scanned_area_rt_grid[0])
    
            valuegrid=[radar.wave_field.eta1,radar.wave_field.eta2]
            vrange=[[-2,2],[-2,2]]
            cmbar=[cm.coolwarm,cm.coolwarm]
    
            # set the contour levels for both plots
            number_of_contour_levels=7
            contour_levels=[]
            for i in range(2):
                contour_levels.append(np.linspace(vrange[i][0], vrange[i][1],
                                                    number_of_contour_levels, endpoint=True))
    
    
            # loop over the axes of the plot and create plot
            for index,ax_contour in enumerate(self.mplcanvas._axes_contours):
                if ax_contour['contour']:
                    for i,coll in enumerate(ax_contour['contour'].collections):
                        logging.debug("remove coll {} ".format(i))
                        ax_contour['contour'].collections.remove(coll)
                    ax_contour['contour'].collections=None
                    ax_contour['contour']=None
    
    
                ax_contour['contour']=ax_contour['ax'].contourf(
                                    radiigrid[index], anglegrid[index],
                                    valuegrid[index],
                                    contour_levels[index],
                                    cmap=cmbar[index], linewidth=0, antialiased=False)
    
    
            # finally, call the redrawing of the canvas
            self.mplcanvas.figure.canvas.draw()
    
  24. eelcovv commented on Mar 9, 2015

    @eelcovv

    To answer my own question, I found a way how to remove the contour plots
    First of all, I noticed tht the for loop over the collection still leaves items in the collection which are called silent_list. This silent list is shorted than the initial collection. So repeating the loop eventually also deletes all the silent_list items. If I introduced a while statement I can repeat the loop over the collection as long as there are no items in the list left. That is the first trick

    The second is that I also have to delete the contours from the axis itself, which I stored in the ax field of the dictionary.

    The part of the code which does the redraw of the contour plot now is:

            for index,ax_contour in enumerate(self.mplcanvas._axes_contours):
                if ax_contour['contour']:
                    while ax_contour['contour'].collections:
                        for coll in ax_contour['contour'].collections:
                            ax_contour['ax'].collections.remove(coll)
                            ax_contour['contour'].collections.remove(coll)
    
    
                    ax_contour['contour'].collections=[]
                    ax_contour['ax'].collections=[]
    
                ax_contour['contour']=ax_contour['ax'].contourf(
                                    radiigrid[index], anglegrid[index],
                                    valuegrid[index],
                                    contour_levels[index],
                                    cmap=cmbar[index], linewidth=0, antialiased=False)
  25. eelcovv commented on Mar 9, 2015

    @eelcovv

    Although the redrawing of the frames is now correct, if I play a movie again I run in the problem that frames got buffered. Now also the trick with the patch does not work. I read you comment about the time needed to redraw the canvas. I can not find exactly in the animation module what I need to do to force a redraw. If I simple add a time.sleep(1) this does not work either. Any hint?

  26. eelcovv commented on Mar 13, 2015

    @eelcovv

    Found the solution here:

    http://stackoverflow.com/questions/9465047/make-an-animated-wave-with-drawpolyline-in-pyside-pyqt

    Just adding

            QtGui.QApplication.processEvents()
            time.sleep(0.0025)
    

    In the time loop after updating the plot prevents blocking the screen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions