Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
zhangruoyu opened this issue Oct 30, 2013 · 25 comments
Closed

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

zhangruoyu opened this issue Oct 30, 2013 · 25 comments
Milestone

Comments

@zhangruoyu
Copy link

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()
@Tillsten
Copy link
Contributor

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

@jason-s
Copy link

jason-s commented Jan 23, 2014

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.

@zhangruoyu
Copy link
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()    

@tacaswell
Copy link
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

@jason-s
Copy link

jason-s commented Feb 21, 2014

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.

@zhangruoyu
Copy link
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.

@tacaswell
Copy link
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)

@Tillsten
Copy link
Contributor

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

@mdboom
Copy link
Member

mdboom commented Feb 24, 2014

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.

@WeatherGod
Copy link
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
.

@tacaswell
Copy link
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.

@mdboom
Copy link
Member

mdboom commented Feb 24, 2014

@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.

@mdboom
Copy link
Member

mdboom commented Feb 24, 2014

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

@efiring
Copy link
Member

efiring commented Feb 24, 2014

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.

@tacaswell
Copy link
Member

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

@tacaswell
Copy link
Member

Closing as #2844 should have fixed this.

@eelcovv
Copy link

eelcovv commented Mar 5, 2015

The patch_qt4agg solution of zhangruoyu works for me. Thanks!

@tacaswell
Copy link
Member

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

@eelcovv
Copy link

eelcovv commented Mar 6, 2015

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

@tacaswell
Copy link
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()

@tacaswell
Copy link
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.

@eelcovv
Copy link

eelcovv commented Mar 9, 2015

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()

@eelcovv
Copy link

eelcovv commented Mar 9, 2015

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)

@eelcovv
Copy link

eelcovv commented Mar 9, 2015

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?

@eelcovv
Copy link

eelcovv commented Mar 13, 2015

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
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants