rxplot.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Based on public domain code from Eli Bendersky (eliben@gmail.com)
00004 # http://eli.thegreenplace.net/2008/08/01/matplotlib-with-wxpython-guis/
00005 # 
00006 # Additions are covered by the license below:
00007 #
00008 # Software License Agreement (BSD License)
00009 #
00010 # Copyright (c) 2009, Willow Garage, Inc.
00011 # All rights reserved.
00012 #
00013 # Redistribution and use in source and binary forms, with or without
00014 # modification, are permitted provided that the following conditions
00015 # are met:
00016 #
00017 #  * Redistributions of source code must retain the above copyright
00018 #    notice, this list of conditions and the following disclaimer.
00019 #  * Redistributions in binary form must reproduce the above
00020 #    copyright notice, this list of conditions and the following
00021 #    disclaimer in the documentation and/or other materials provided
00022 #    with the distribution.
00023 #  * Neither the name of Willow Garage, Inc. nor the names of its
00024 #    contributors may be used to endorse or promote products derived
00025 #    from this software without specific prior written permission.
00026 #
00027 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00028 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00029 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00030 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00031 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00032 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00033 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00034 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00035 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00036 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00037 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00038 # POSSIBILITY OF SUCH DAMAGE.
00039 #
00040 # Revision $Id$
00041 
00042 PKG = 'rxtools'
00043 import collections
00044 import os
00045 import string
00046 import sys
00047 import wx
00048 
00049 import roslib.names
00050 import roslib.packages
00051 
00052 import rospy
00053 
00054 import rxtools.vizutil
00055 rxtools.vizutil.check_matplotlib_deps()
00056 
00057 ## MPL imports
00058 
00059 import matplotlib
00060 matplotlib.use('WXAgg')
00061 from matplotlib.figure import Figure
00062 from matplotlib.backends.backend_wxagg import \
00063     FigureCanvasWxAgg as FigCanvas, \
00064     NavigationToolbar2WxAgg as NavigationToolbar
00065 
00066 import numpy as np
00067 import pylab
00068 
00069 from rxtools.rosplot import ROSData, is_ros_pause, is_ros_stop, toggle_ros_pause, set_ros_stop
00070 import mpl_toolkits.mplot3d.art3d as art3d
00071 
00072 ## Plotting colors. We artificially constrain the number of allowed plots by the number of
00073 ## COLORS we specify. rxplot has not been stress-tested yet
00074 COLORS = (1, 0, 0), (0, 0, 1), (0, 1, 0), (1, 0, 1), (0, 1, 1), (0.5, 0.24, 0), (0, 0.5, 0.24), (1, 0.5, 0), 
00075 
00076 # TODO: rewrite so that pause button toggles activation of other buttons
00077 ## Toolbar controls for rxplot
00078 class RxPlotToolbar(NavigationToolbar):
00079     ON_PAUSE = wx.NewId()
00080     ON_STOP = wx.NewId()
00081     def __init__(self, canvas):
00082         NavigationToolbar.__init__(self, canvas)
00083         
00084         self.play_bm = wx.Bitmap(roslib.packages.get_pkg_dir(PKG) + '/icons/play-16.png')
00085         self.pause_bm = wx.Bitmap(roslib.packages.get_pkg_dir(PKG) + '/icons/pause-16.png')
00086         self.AddSimpleTool(self.ON_PAUSE, self.pause_bm, 'Pause', 'Activate pause')
00087         wx.EVT_TOOL(self, self.ON_PAUSE, self._on_pause)
00088 
00089         self.AddSimpleTool(self.ON_STOP, wx.Bitmap(roslib.packages.get_pkg_dir(PKG) + '/icons/stop-16.png'), 'Stop', 'Activate stop')
00090 
00091         wx.EVT_TOOL(self, self.ON_STOP, self._on_stop)
00092         self._set_tb_enable()
00093 
00094     ## Most toolbar buttons remain disabled until rxplot enters paused or stopped state.
00095     def _set_tb_enable(self):
00096         # parent toolbar doesn't expose home, save, or subplot
00097         # buttons. However, these don't behave as badly under pause so
00098         # it's not as important to disable those.
00099         tb_enabled = is_ros_pause() or is_ros_stop()
00100         try:
00101             buttons = [self.wx_ids['Back'], self.wx_ids['Forward'], self.wx_ids['Pan'], self.wx_ids['Zoom']]
00102         except AttributeError:
00103             buttons = [self._NTB2_BACK, self._NTB2_FORWARD, self._NTB2_PAN, self._NTB2_ZOOM]
00104         for b in buttons:
00105             self.EnableTool(b, tb_enabled)
00106 
00107     def _on_pause(self, evt):
00108         toggle_ros_pause()
00109         self._set_tb_enable()
00110         if is_ros_pause():
00111             self.SetToolNormalBitmap(self.ON_PAUSE, self.play_bm)
00112         else:
00113             self.SetToolNormalBitmap(self.ON_PAUSE, self.pause_bm)
00114         # cancel state of pan/zoom
00115         try:
00116             self.ToggleTool(self.wx_ids['Pan'], False)
00117             self.ToggleTool(self.wx_ids['Zoom'], False)
00118         except AttributeError:
00119             self.ToggleTool(self._NTB2_PAN, False)
00120             self.ToggleTool(self._NTB2_ZOOM, False)
00121         self._active = 'ZOOM'
00122         self.zoom(tuple())
00123 
00124     def _on_stop(self, evt):
00125         set_ros_stop()
00126         self._set_tb_enable()
00127         # kill pause/stop buttons as well
00128         self.EnableTool(self.ON_PAUSE, False)
00129         self.EnableTool(self.ON_STOP, False)        
00130             
00131 ## WX Frame containing plots
00132 class RxPlotFrame(wx.Frame):
00133     def __init__(self, topics, options):
00134         self.title = options.title
00135         self.legend = options.legend
00136         self.mode = options.mode
00137         self.miny = options.miny
00138         self.maxy = options.maxy
00139         if self.mode == '2d':
00140             self.draw_plot = self.draw_plot_2d
00141             self.init_plot = self.init_plot_2d
00142         elif self.mode in ['3d', 'scatter']:
00143             self.draw_plot = self.draw_plot_3d
00144             self.init_plot = self.init_plot_3d
00145         else:
00146             raise ValueError("Mode must be one of [2d, 3d, scatter], not [%s]"%(options.mode))
00147         
00148         wx.Frame.__init__(self, None, -1, self.title)
00149 
00150         # There are two different indicies for our list data types based on the fact that topics is
00151         # a list[list(str)]. 
00152         #
00153         # axes_index: first index into topics
00154         #
00155         # plot_index: index based on flattened topic list, i.e. absolute topic number
00156         #
00157         # e.g. for self.topics = [ ['/p0/f0'], ['/p1/f1', '/p2/f2']]:
00158         #  * axes_index 0 indexes ['/p0/f0'], 
00159         #  * axes_index 1 indexes ['/p1/f1', '/p2/f2'],
00160         #  * plot_index 0 indexes '/p0/f0'
00161         #  * plot_index 1 indexes '/p1/f1'
00162         #  * plot_index 2 indexes '/p2/f2'
00163         
00164         # self.axes and self.topics uses axes_index
00165         self.axes = []
00166         self.topics = topics
00167         self.period = options.period
00168         self.marker = options.marker
00169         
00170         # initialize data storage and sources
00171         # datagen, datax, and datay use plot_index.
00172         self.datagen = []
00173         self.datax = []
00174         self.datay = []
00175         self.buffer_size = options.buffer     
00176         self.redraw_period = int(1. / options.refresh_rate * 1000) # convert to ms
00177         
00178         self.start_time = rospy.get_time()
00179         for topic_list in topics:
00180             for t in topic_list:
00181                 dg = ROSData(t, self.start_time)
00182                 self.datagen.append(dg)
00183                 datax, datay = dg.next()
00184                 self.datax.append(collections.deque(datax))
00185                 self.datay.append(collections.deque(datay))
00186         
00187         self.create_menu()
00188         self.statusbar = self.CreateStatusBar()
00189         self.create_main_canvas()
00190         
00191         self.redraw_timer = wx.Timer(self)
00192         self.Bind(wx.EVT_TIMER, self.on_redraw_timer, self.redraw_timer)
00193         
00194         #self.redraw_timer.Start(100)
00195         self.redraw_timer.Start(self.redraw_period)
00196         if options.start_paused:
00197             self.toolbar._on_pause(None)
00198 
00199     def create_menu(self):
00200         self.menubar = wx.MenuBar()
00201         
00202         menu_file = wx.Menu()
00203         m_expt = menu_file.Append(-1, "&Save plot\tCtrl-S", "Save plot to file")
00204         self.Bind(wx.EVT_MENU, self.on_save_plot, m_expt)
00205         menu_file.AppendSeparator()
00206         m_exit = menu_file.Append(-1, "E&xit\tCtrl-X", "Exit")
00207         self.Bind(wx.EVT_MENU, self.on_exit, m_exit)
00208                 
00209         self.menubar.Append(menu_file, "&File")
00210         self.SetMenuBar(self.menubar)
00211 
00212     def onpick(self, event):
00213         thisline = event.artist
00214         xdata, ydata = thisline.get_data()
00215         ind = event.ind
00216         self.statusbar.SetStatusText("%s, %s" % (xdata[ind][len(xdata[ind])/2], ydata[ind][len(ydata[ind])/2]))
00217 
00218     def create_main_canvas(self):
00219         # Create main figure
00220         self.dpi = 100
00221         params = matplotlib.figure.SubplotParams(left=0.125, bottom=0.12, right=0.99, top=0.99, wspace=0.001, hspace=0.1)
00222         self.fig = Figure((3.0, 3.0), dpi=self.dpi, subplotpars=params)
00223 
00224         self.canvas = FigCanvas(self, -1, self.fig)
00225         self.canvas.mpl_connect('pick_event', self.onpick)
00226 
00227         self.init_plot()
00228         
00229         self.sizer = wx.BoxSizer(wx.VERTICAL)
00230         self.sizer.Add(self.canvas, 1, flag=wx.LEFT | wx.TOP | wx.GROW)
00231         self.SetSizer(self.sizer)
00232 
00233         self.add_toolbar()        
00234 
00235         self.SetSize(wx.Size(500, 700))
00236         self.Layout()
00237     
00238     def init_plot_2d(self):
00239         self.plot_data = []
00240         flat_topics = []
00241 
00242         axes_index = 0
00243         plot_index = 0
00244 
00245         for topic_list in self.topics:
00246             axes = self.fig.add_subplot(string.atoi('%d1%d'%(len(self.topics), axes_index+1))) 
00247             axes.set_axis_bgcolor('white')            
00248             #axes.set_title(', '.join(topic_list), size=8)
00249             pylab.setp(axes.get_xticklabels(), fontsize=6)
00250             pylab.setp(axes.get_yticklabels(), fontsize=8)
00251             # equiv to self.axes[axes_index] = axes
00252             self.axes.append(axes)
00253             axes_index += 1
00254 
00255             for topic in topic_list:
00256                 flat_topics.append(topic)
00257                 plot_data = \
00258                           axes.plot(self.datax[plot_index],
00259                                     self.datay[plot_index],
00260                                     marker=self.marker,
00261                                     linewidth=1,
00262                                     picker=5,
00263                                     color=COLORS[plot_index],
00264                                     )[0]
00265                 # equiv to self.plot_data[plot_index] = plot_data
00266                 self.plot_data.append(plot_data)
00267                 plot_index += 1
00268 
00269         # Set grid visibility properties
00270         for ax in self.axes:
00271             ax.grid(True, color='gray')
00272             pylab.setp(ax.get_xticklabels(), visible=True)
00273             
00274         # Make a legend
00275         fp = matplotlib.font_manager.FontProperties(size=8)
00276         legends = self.legend.split(",") if self.legend else []
00277         if legends:
00278             if len(legends) != len(flat_topics):
00279                 raise "Number of legend strings does not match the number of topics"
00280             self.fig.legend(self.plot_data, legends, 'lower right', prop=fp)
00281         elif len(flat_topics) > 1:
00282             self.fig.legend(self.plot_data, flat_topics, 'lower right', prop=fp)
00283 
00284     def init_plot_3d(self):
00285         # init plot_data (only used for 3d mode right now until scatter is ported)
00286         self.plot_data = []
00287         # choose a color (for scatter mode only)
00288         import random
00289         self.color = COLORS[random.randint(0, len(COLORS))]
00290         
00291         # initialize 3d axes from mplot3d
00292         import mpl_toolkits.mplot3d.axes3d
00293         self.ax = mpl_toolkits.mplot3d.axes3d.Axes3D(self.fig)
00294 
00295         flat_topics = []
00296         for topic_list in self.topics:
00297             for topic in topic_list:
00298                 flat_topics.append(topic)
00299                 
00300         ntopics = len(flat_topics)
00301         if ntopics <= 3:
00302             self.ax.set_xlabel(flat_topics[0])
00303             self.ax.set_ylabel(flat_topics[1])
00304         if ntopics == 2:
00305             self.ax.set_zlabel("time")
00306         elif ntopics == 3:
00307             self.ax.set_zlabel(flat_topics[2])
00308         else:
00309             raise Exception("Expected 2 or 3 topics, but got %d" % (ntopics))
00310 
00311         # attach mouse handler (must be called after FigCanvas)
00312         self.ax.mouse_init()
00313 
00314     def draw_plot_2d(self, relimit=False):
00315         if not self.plot_data:
00316             return
00317         
00318         # Set axis bounds
00319         if relimit and self.datax[0]:
00320             axes_index = 0
00321             plot_index = 0
00322             # axes are indexed by topic_list, plots are indexed by topic number
00323             for topic_list in self.topics:
00324                 axes = self.axes[axes_index] 
00325                 axes_index += 1
00326                 ymin = self.miny
00327                 ymax = self.maxy
00328                 for t in topic_list:
00329                     datax = self.datax[plot_index]
00330                     datay = self.datay[plot_index]
00331                     plot_index += 1
00332 
00333                     xmax = datax[-1]
00334                     xmin = xmax - self.period
00335                     if ymin==0 and ymax==0:
00336                         ymin = min(datay)
00337                         ymax = max(datay)
00338                     else:
00339                         ymin = min(min(datay), ymin)
00340                         ymax = max(max(datay), ymax)
00341 
00342                     # pad the min/max
00343                     delta = ymax - ymin
00344                     # fix wrong scaling of axis for zero range using a synthetic delta (see #2046)
00345                     if delta == 0:
00346                         delta = ymax
00347                     ymin -= .05*delta
00348                     ymax += .05*delta
00349                     
00350                     axes.set_xbound(lower=xmin, upper=xmax)
00351                     axes.set_ybound(lower=ymin, upper=ymax)
00352 
00353         # Set plot data on current axes
00354         for plot_index in xrange(0, len(self.plot_data)):
00355             datax = self.datax[plot_index]
00356             datay = self.datay[plot_index]
00357 
00358             plot_data = self.plot_data[plot_index]
00359             plot_data.set_data(np.array(datax), np.array(datay))
00360 
00361         self.canvas.draw()
00362 
00363     def draw_plot_3d(self, relimit=False):
00364         # Don't have to relimit 3d plots
00365         if self.datax[0] and self.datay[0] and self.datay[1]:
00366 
00367             ndata = len(self.datay)
00368             if ndata >=2:
00369                 datax = np.array(self.datay[0])
00370                 datay = np.array(self.datay[1])
00371         
00372             # Plot time from topic 1 on z if no third topic is provided
00373             if ndata == 2:
00374                 dataz = np.array(self.datax[0])
00375             elif ndata == 3 and self.datay[2]:
00376                 dataz = np.array(self.datay[2])
00377             else:
00378                 print "unexpected input data dimensions for 3d plotting (%d)" % ndata
00379 
00380             if self.mode == '3d':
00381                 if not self.plot_data:
00382                     self.plot_data = self.ax.plot(datax, datay, dataz)
00383 
00384                 self.plot_data[0].set_data(datax, datay)
00385                 art3d.line_2d_to_3d(self.plot_data[0], zs=dataz, zdir='z')
00386                 self.ax.auto_scale_xyz(datax, datay, dataz, True)
00387             else:
00388                 # 'blue' is arbitrary, just have to keep mpl from choosing different each time.
00389                 #
00390                 # kwc: I haven't figured out how to performance tune
00391                 # scatter plots the same way I tuned the 3d plot. The
00392                 # MPL APIs are a bit haphazard when it comes to 3d
00393                 # plotting.
00394                 self.plot_data = self.ax.scatter(datax, datay, dataz, color=self.color)
00395 
00396             self.canvas.draw()
00397     
00398     def on_save_plot(self, event):
00399         file_choices = "PNG (*.png)|*.png"
00400         
00401         dlg = wx.FileDialog(
00402             self, 
00403             message="Save plot as...",
00404             defaultDir=os.getcwd(),
00405             defaultFile="plot.png",
00406             wildcard=file_choices,
00407             style=wx.SAVE)
00408         
00409         if dlg.ShowModal() == wx.ID_OK:
00410             path = dlg.GetPath()
00411             self.canvas.print_figure(path, dpi=self.dpi)
00412             self.flash_status_message("Saved to %s" % path)
00413 
00414     def on_redraw_timer(self, event):
00415         # if paused do not add data, but still redraw the plot
00416         # (to respond to scale modifications, grid change, etc.)
00417         #
00418         if is_ros_stop():
00419           for dg in self.datagen:
00420             dg.close()
00421           self.datagen = []
00422 
00423         if not is_ros_pause():
00424             for plot_index in xrange(0, len(self.datagen)):
00425                 try:
00426                     datax, datay = self.datagen[plot_index].next()
00427                 except Exception, e:
00428                     print >> sys.stderr, str(e)
00429                     wx.GetApp().Exit()
00430                     return
00431 
00432                 plot_datax, plot_datay = self.datax[plot_index], self.datay[plot_index]
00433                 plot_datax.extend(datax)
00434                 plot_datay.extend(datay)
00435 
00436                 if len(plot_datax) > 0 and self.buffer_size > 0:
00437                     xcutoff = plot_datax[-1] - self.buffer_size
00438                     while len(plot_datax) > 0 and plot_datax[0] < xcutoff:
00439                         plot_datax.popleft()
00440                         plot_datay.popleft()
00441 
00442             self.draw_plot(relimit=True)
00443     
00444     def on_exit(self, event):
00445         self.Destroy()
00446     
00447     def flash_status_message(self, msg, flash_len_ms=1500):
00448         self.statusbar.SetStatusText(msg)
00449         self.timeroff = wx.Timer(self)
00450         self.Bind(
00451             wx.EVT_TIMER, 
00452             self.on_flash_status_off, 
00453             self.timeroff)
00454         self.timeroff.Start(flash_len_ms, oneShot=True)
00455     
00456     def on_flash_status_off(self, event):
00457         self.statusbar.SetStatusText('')
00458 
00459     def add_toolbar(self):
00460         self.toolbar = RxPlotToolbar(self.canvas)
00461         self.toolbar.Realize()
00462         self.SetToolBar(self.toolbar)
00463         if 0:
00464             if wx.Platform == '__WXMAC__':
00465                 # Mac platform (OSX 10.3, MacPython) does not seem to cope with
00466                 # having a toolbar in a sizer. This work-around gets the buttons
00467                 # back, but at the expense of having the toolbar at the top
00468                 self.SetToolBar(self.toolbar)
00469             else:
00470                 # On Windows platform, default window size is incorrect, so set
00471                 # toolbar width to figure width.
00472                 tw, th = self.toolbar.GetSizeTuple()
00473                 fw, fh = self.canvas.GetSizeTuple()
00474                 # By adding toolbar in sizer, we are able to put it at the bottom
00475                 # of the frame - so appearance is closer to GTK version.
00476                 # As noted above, doesn't work for Mac.
00477                 self.toolbar.SetSize(wx.Size(fw, th))
00478                 self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND)
00479         # update the axes menu on the toolbar
00480         self.toolbar.update()
00481 
00482     
00483 
00484 def rxplot_app(topic_list, options):
00485     rospy.init_node('rxplot', anonymous=True)
00486 
00487     try:
00488         app = wx.PySimpleApp()    
00489         app.frame = rxtools.rxplot.RxPlotFrame(topic_list, options)
00490         app.frame.Show()
00491         app.MainLoop()
00492     except Exception, e:
00493         rospy.logerr(e)
00494         print >> sys.stderr, str(e)
00495         wx.GetApp().Exit()
00496     
00497     rospy.signal_shutdown('GUI shutdown')    
00498 


rxtools
Author(s): Josh Faust, Rob Wheeler, Ken Conley
autogenerated on Mon Oct 6 2014 07:25:59