Skip to content

Image redraw problem on OSX with wx>2.9 & mpl>1.1 #3734

Closed

Description

@briantoby

I have seen a problem on more than one Mac running OS X 10.9.x where MPL images are not updated properly. As one example of this, when one zooms a plot, by selecting the zoom widget, the rectangle is never erased and after it is "released" the plot is never redrawn with the new limits. This happens when I embed a figure into a wx Panel, as shown in the script below.

I do not see the problem with EPD (python 2.7.3, wxpython 2.8.10.1, matplotlib 1.1.0), but do with fairly recent Anaconda (python 2.7.8, wxpython 3.0.0.0, matplotlib 1.4.2) and Canopy (python 2.7.6, wxpython 2.9.2.4, matplotlib 1.3.1) dists.

mplbug

# simple demo that fails on Mac (at least on 10.9.5) and later wx/mpl combos
#
# problem is that if one selects the zoom button and drags a region,
# the rectangles never disappear and the image is not updated to
# show the zoomed in region
#
# works (not quite 100%) w/EPD (python 2.7.3, wxpython 2.8.10.1, matplotlib 1.1.0)
# fails w/Anaconda (python 2.7.8, wxpython 3.0.0.0, matplotlib 1.4.2
# fails w/Canopy (python 2.7.6, wxpython 2.9.2.4, matplotlib 1.3.1)
#

import sys
import wx
import wx.aui
import matplotlib as mpl
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as mplCanvas
from matplotlib.backends.backend_wxagg import NavigationToolbar2Wx as mplToolbar
print "python:     ",sys.version[:5]
print "wxpython:   ",wx.__version__
print "matplotlib: ",mpl.__version__

app = wx.PySimpleApp()

# create frame with notebook
frame = wx.Frame(None, size=(400,400)) 
win = wx.Panel(frame)
win.nb = wx.aui.AuiNotebook(win)
sizer = wx.BoxSizer(wx.VERTICAL) 
sizer.Add(win.nb, 1, wx.EXPAND)
win.SetSizer(sizer)

# create a panel for the plot with a plot in it
tab = wx.Panel(win.nb, id=-1)
figure = mpl.figure.Figure(dpi=None)
# create a quick figure for the plot
ax = figure.gca()
ax.plot(range(10),range(10),'o-')
canvas = mplCanvas(tab, -1, figure) 
toolbar = mplToolbar(canvas)
toolbar.Realize()
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(canvas,1,wx.EXPAND)
sizer.Add(toolbar, 0 , wx.LEFT | wx.EXPAND)
tab.SetSizer(sizer)
win.nb.AddPage(tab,'plot1') # add the panel as a notebook tab

frame.Show()
app.MainLoop()

Activity

  1. added this to the v1.4.3 milestone on Oct 28, 2014
  2. briantoby commented on Nov 26, 2014

    @briantoby
    Author

    I have spent some more time on this and have noticed that in _onDrawIdle() in backend_wx.py, the wx.GetApp().Pending() call seems to always evaluate as True.

    I have also seen that I can find a similar bug in Windows w/only 64 bit wx, where after the first zoom, home or arrow use, .Pending also never stops returning as True.

    Patching draw_idle() to call self.draw() directly seems to solve at least some of the problems:

    def draw_idle(self):
        """
        Delay rendering until the GUI is idle.
        """
        DEBUG_MSG("draw_idle()", 1, self)
        self._isDrawn = False  # Force redraw
        if major >= 2 or (major == 2 and minor >= 8):
            wx.CallAfter(self.draw)
        # Create a timer for handling draw_idle requests
        # If there are events pending when the timer is
        # complete, reset the timer and continue.  The
        # alternative approach, binding to wx.EVT_IDLE,
        # doesn't behave as nicely.
        elif hasattr(self,'_idletimer'):
         ...
    

    I suspect this causes other problems. This also does not address the uglyness when dragging the zoom box.

  3. RobinD42 commented on Dec 1, 2014

    @RobinD42
    Contributor

    The docstring for draw_idle is: "Delay rendering until the GUI is idle." To do that in wx you do not need to mess with Pending and timers at all. Simply using Refresh will do basically the same thing. It causes the system to add a paint event to the queue, but it will not be processed immediately. It will typically be delayed until there are no other pending events, or until painting is forced for some reason. On OSX the paint event will be scheduled into the active display pipeline so it will happen at the best possible moment taking the whole display into account.

    On a former job I worked around this issue by subclassing FigureCanvasWxAgg and overriding draw_idle like this:

        def draw_idle(self, *args, **kw):
            # Instead of messing with timers and wx.App.Pending, just trigger a
            # paint event.
            self._isDrawn = False
            self.Refresh(eraseBackground=False)
    

    It worked well for at least the use cases needed by that application (several histogram, scatter and img plots) and was tested on Windows and OSX. wxGTK ports should also do fine with this change.

    NOTE: the above will not fix the trails left behind while dragging the rubberband. I have another fix for that which I'll try to track down.

  4. RobinD42 commented on Dec 1, 2014

    @RobinD42
    Contributor

    To get a better rubberband effect that also doesn't leave trails on OSX, you can use a wx.Overlay instead of directly drawing to a wx.ClientDC (which is just a bad idea in most cases these days.) I've implemented this in an old project by overriding the following methods in a class derived from NavigationToolbar2Wx. This should work in at least wxPython 2.8.12, 2.9, and 3.0+, and possibly earlier 2.x versions. You may want to encorporate these changes directly into NavigationToolbar2Wx.

        def press(self, event):
            if self._active == 'ZOOM':
                self.wxoverlay = wx.Overlay()
    
        def release(self, event):
            if self._active == 'ZOOM':
                # When the mouse is released we reset the overlay and it
                # restores the former content to the window.
                self.wxoverlay.Reset()
                del self.wxoverlay
    
        def draw_rubberband(self, event, x0, y0, x1, y1):
            # Use an Overlay to draw a more awesome rubberband.
    
            dc = wx.ClientDC(self.canvas)
            odc = wx.DCOverlay(self.wxoverlay, dc)
            odc.Clear()
    
            # Mac's DC is already the same as a GCDC, and it causes
            # problems with the overlay if we try to use an actual
            # wx.GCDC so don't try it.
            if 'wxMac' not in wx.PlatformInfo:
                dc = wx.GCDC(dc)
    
            height = self.canvas.figure.bbox.height
            y1 = height - y1
            y0 = height - y0
    
            if y1<y0: y0, y1 = y1, y0
            if x1<y0: x0, x1 = x1, x0
    
            w = x1 - x0
            h = y1 - y0
            rect = wx.Rect(x0, y0, w, h)
    
            rubberBandColor = '#C0C0FF' # or load from config?
    
            # Set a pen for the border
            color = wx.NamedColour(rubberBandColor)
            dc.SetPen(wx.Pen(color, 1))
    
            # use the same color, plus alpha for the brush
            r, g, b = color.Get()
            color.Set(r,g,b, 0x60)
            dc.SetBrush(wx.Brush(color))
            dc.DrawRectangleRect(rect)
    
  5. efiring commented on Dec 1, 2014

    @efiring
    Member

    @RobinD42 Thanks very much for the tips. If you would like to turn them into a PR, that would be even better; but if not, I expect they will get in one way or another.

  6. RobinD42 commented on Dec 2, 2014

    @RobinD42
    Contributor

    Yeah, I figured that would be the first question. 😉

    I didn't have time to do a real PR today, but I'll try to do it soonish.

  7. tacaswell commented on Jan 8, 2015

    @tacaswell
    Member

    closed by #3905

  8. modified the milestones: v1.4.x, v1.4.3 on Jan 15, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions