5

I have a matplotlib figure/canvas in a wxpython window. I want to update some information on the plot as the mouse moves around. I've connected to 'motion_notify_event' to get this information.

In the code below, a lot of random data is plotted and then the x,y location of the cursor is displayed in the statusbar of the window. This is very smooth and works well. However, I really want to display this information at the top of the plot. The behavior I want is shown if you uncomment the last two lines of cbUpdateCursor. However, when this is done, the response time to moving the cursor is terribly slow (because draw gets called and there is a lot of data, but draw must be called or the text doesn't get updated).

How can I speed this up so the cursor position can be displayed on the plot, but not slow it down so much? I think I might need to do something with bbox?

Code:

import wx
import numpy as np
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.widgets import Cursor
from matplotlib.backends.backend_wxagg import \
   FigureCanvasWxAgg as FigCanvas, \
   NavigationToolbar2WxAgg as NavigationToolbar

class wxPlotting(wx.Frame):
   title = 'Test'
   def __init__(self):
      wx.Frame.__init__(self, None, -1, self.title)
      self.time = np.arange(10000)
      self.data = np.random.random(10000)
      self.sb = self.CreateStatusBar()
      self.create_main_panel()
      self.axes.plot(self.time, self.data)
      self.canvas.draw()

   def create_main_panel(self):
      self.panel = wx.Panel(self)
      self.fig = Figure((5.0, 4.0), dpi=100)
      self.canvas = FigCanvas(self.panel, -1, self.fig)
      self.axes = self.fig.add_subplot(111)
      self.text = self.axes.text(0., 1.005, '', transform = self.axes.transAxes)
      self.cursor = Cursor(self.axes, useblit=True, color='red')
      self.canvas.mpl_connect('motion_notify_event', self.cbUpdateCursor)
      self.vbox = wx.BoxSizer(wx.VERTICAL)
      self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
      self.panel.SetSizer(self.vbox)
      self.vbox.Fit(self)

   def cbUpdateCursor(self, event):
      if event.inaxes:
         text = 'x = %5.4f, y = %5.4f' % (event.xdata, event.ydata)
         self.sb.SetStatusText(text)
         #self.text.set_text(text)
         #self.canvas.draw()

if __name__ == '__main__':
   app = wx.PySimpleApp()
   app.frame = wxPlotting()
   app.frame.Show()
   app.MainLoop()

Basically I want something similar to the text that gets displayed using pyplot, i.e. the bottom right corner when the code below is run:

Code:

import matplotlib.pyplot as plt
plt.plot(range(10000), range(10000))
plt.show()

EDIT:

In my actual program, I want the static text to be within the matplotlib axes, not really above it. So I don't think I can just use a wxpython statictext to display it.

Scott B
  • 2,542
  • 7
  • 30
  • 44

2 Answers2

5

You could use blitting, similar to the animation examples here.

This make a very large performance difference in this case, as only a small portion of the window needs to be redrawn.

Unfortunately, I can't figure out how to get a gray background behind the text when it's redrawn, to match the default figure background behind it... The performance is excellent, though.

As a stand-alone example based on your code above:

import wx
import numpy as np
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.widgets import Cursor
from matplotlib.backends.backend_wxagg import \
   FigureCanvasWxAgg as FigCanvas, \
   NavigationToolbar2WxAgg as NavigationToolbar

class wxPlotting(wx.Frame):
   title = 'Test'
   def __init__(self):
      wx.Frame.__init__(self, None, -1, self.title)
      self.time = np.arange(10000)
      self.data = np.random.random(10000)
      self.sb = self.CreateStatusBar()
      self.create_main_panel()
      self.axes.plot(self.time, self.data)
      self.background = self.canvas.copy_from_bbox(self.fig.bbox)
      self.canvas.draw()

   def create_main_panel(self):
      self.panel = wx.Panel(self)
      self.fig = Figure((5.0, 4.0), dpi=100)
      self.canvas = FigCanvas(self.panel, -1, self.fig)
      self.axes = self.fig.add_subplot(111)
      self.text = self.axes.text(0., 1.005, '', transform = self.axes.transAxes, animated=True)
      self.cursor = Cursor(self.axes, useblit=True, color='red')
      self.canvas.mpl_connect('motion_notify_event', self.cbUpdateCursor)
      self.vbox = wx.BoxSizer(wx.VERTICAL)
      self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
      self.panel.SetSizer(self.vbox)
      self.vbox.Fit(self)

   def cbUpdateCursor(self, event):
      if event.inaxes:
         text = 'x = %5.4f, y = %5.4f' % (event.xdata, event.ydata)
         self.sb.SetStatusText(text)

         self.canvas.restore_region(self.background)
         self.text.set_text(text)
         self.axes.draw_artist(self.text)
         self.canvas.blit(self.text.get_window_extent())

if __name__ == '__main__':
   app = wx.PySimpleApp()
   app.frame = wxPlotting()
   app.frame.Show()
   app.MainLoop()
Joe Kington
  • 275,208
  • 71
  • 604
  • 463
  • @Joe Kington: Interestingly...this works as described on Linux, but at my work computer (windows xp), it works totally differently. On windows, it does get replaced with that default gray background, but it seems to not get replaced on every loop. The numbers are mostly unreadable as they pile on top of each other. – Scott B May 24 '11 at 16:21
  • Actually, if you switch the last two lines of init (call self.canvas.draw before self.background=...), then it works correctly on Windows, with the right background color. I'll have to try it out later to see if that fixes the background thing on Linux too. – Scott B May 24 '11 at 16:32
  • @Scott B - Huh! I only tested it on linux. If you have time, it's probably worth filing a bug report and/or asking on the mailing list. I'm not terribly familiar with wx (I need to learn it. It really looks like a nice GUI toolkit.), but there may be some well known workarounds. Also, it's entirely possible that I'm doing things in a less-than-efficient manner. There may be a better way to go about this. – Joe Kington May 24 '11 at 16:33
  • Ya, I'll continue to look into it. I also noticed that it gets messed up (wrong information in self.background) when it gets re-sized (again, on Windows anyway). – Scott B May 24 '11 at 16:35
  • @Scott - Ah! That fixes the background problem on linux, as well! This is probably a case of me not being terribly familiar with things rather than a bug, then. It makes sense, as matplotlib doesn't initialize the renderer for the canvas until it's drawn the first time. – Joe Kington May 24 '11 at 16:36
  • @Joe: Ya, that's what gave me the clue to do that. And for the re-size problem, I'd probably have to capture the re-size event and then reset self.background after the background is redrawn. Thanks for the help Joe. – Scott B May 24 '11 at 16:45
0

You could add a static text box on top, and just update it's label:

import wx
import numpy as np
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.widgets import Cursor
from matplotlib.backends.backend_wxagg import \
   FigureCanvasWxAgg as FigCanvas, \
   NavigationToolbar2WxAgg as NavigationToolbar

class wxPlotting(wx.Frame):
   title = 'Test'
   def __init__(self):
      wx.Frame.__init__(self, None, -1, self.title)
      self.time = np.arange(10000)
      self.data = np.random.random(10000)
      self.sb = self.CreateStatusBar()
      self.create_main_panel()
      self.axes.plot(self.time, self.data)
      self.canvas.draw()

   def create_main_panel(self):
      self.panel = wx.Panel(self)
      self.fig = Figure((5.0, 4.0), dpi=100)
      self.canvas = FigCanvas(self.panel, -1, self.fig)
      self.axes = self.fig.add_subplot(111)
      self.text = self.axes.text(0., 1.005, '', transform = self.axes.transAxes)
      self.cursor = Cursor(self.axes, useblit=True, color='red')
      self.canvas.mpl_connect('motion_notify_event', self.cbUpdateCursor)
      self.vbox = wx.BoxSizer(wx.VERTICAL)
      self.cursor_pos = wx.StaticText(self.panel,-1, label="")
      self.vbox.Add(self.cursor_pos, 0, wx.LEFT | wx.TOP | wx.GROW)
      self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
      self.panel.SetSizer(self.vbox)
      self.vbox.Fit(self)

   def cbUpdateCursor(self, event):
      if event.inaxes:
         text = 'x = %5.4f, y = %5.4f' % (event.xdata, event.ydata)
         self.sb.SetStatusText(text)
         self.cursor_pos.SetLabel(text)
         #self.text.set_text(text)
         #self.canvas.draw()

if __name__ == '__main__':
   app = wx.PySimpleApp()
   app.frame = wxPlotting()
   app.frame.Show()
   app.MainLoop()
Gerrat
  • 28,863
  • 9
  • 73
  • 101
  • Ya, I did consider this, but I really want to do it with matplotlib. Because the ACTUAL position I'll use in my program is going to be within the matplotlib axes themselves. – Scott B May 20 '11 at 20:42