Package diffpy :: Package pdfgui :: Package gui :: Module extendedplotframe
[hide private]
[frames] | no frames]

Source Code for Module diffpy.pdfgui.gui.extendedplotframe

  1  #!/usr/bin/env python 
  2  ############################################################################## 
  3  # 
  4  # PDFgui            by DANSE Diffraction group 
  5  #                   Simon J. L. Billinge 
  6  #                   (c) 2006 trustees of the Michigan State University. 
  7  #                   All rights reserved. 
  8  # 
  9  # File coded by:    Jiwu Liu, Chris Farrow 
 10  # 
 11  # See AUTHORS.txt for a list of people who contributed. 
 12  # See LICENSE.txt for license information. 
 13  # 
 14  ############################################################################## 
 15   
 16  """ 
 17  The module contains extensions for GUI plot frame. 
 18  """ 
 19   
 20  __id__ = "$Id: extendedplotframe.py 3018 2009-04-07 19:45:51Z juhas $" 
 21   
 22  import os.path 
 23   
 24  import matplotlib 
 25  matplotlib.use('WXAgg') 
 26  from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas 
 27  from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg as NavToolbar 
 28  from matplotlib.figure import Figure 
 29  from matplotlib.backends.backend_wx import _load_bitmap 
 30  from matplotlib.artist import setp 
 31  from matplotlib.font_manager import FontProperties 
 32  import wx 
 33   
 34   
 35  DATA_SAVE_ID  = wx.NewId() 
36 -class ExtendedToolbar(NavToolbar):
37 """An extended plotting toolbar with a save and close button."""
38 - def __init__(self, canvas, cankill):
39 if wx.Platform != '__WXMAC__': 40 NavToolbar.__init__(self, canvas) 41 else: 42 _realizer = self.Realize 43 def f(): pass 44 self.Realize = f 45 NavToolbar.__init__(self, canvas) 46 self.Realize = _realizer 47 48 # Get rid of the configure subplots button 49 self.DeleteToolByPos(6) 50 51 # Add new buttons 52 self.AddSimpleTool(wx.ID_PRINT, 53 wx.ArtProvider.GetBitmap(wx.ART_PRINT, wx.ART_TOOLBAR), 54 'Print', 'print graph') 55 self.AddSimpleTool(DATA_SAVE_ID, 56 _load_bitmap('stock_save_as.xpm'), 57 'Export plot data', 'Export plot data to file') 58 self.AddSeparator() 59 self.AddSimpleTool(wx.ID_CLOSE, 60 _load_bitmap('stock_close.xpm'), 61 'Close window', 'Close window')
62
63 - def save(self, evt):
64 # Fetch the required filename and file type. 65 filetypes = self.canvas._get_imagesave_wildcards() 66 exts = [] 67 # sortedtypes put png in the first 68 sortedtypes = [] 69 import re 70 types = filetypes[0].split('|') 71 n = 0 72 for ext in types[1::2]: 73 # Extract only the file extension 74 res = re.search(r'\*\.(\w+)', ext) 75 pos = n*2 76 if re.search(r'png', ext): 77 pos = 0 78 sortedtypes.insert(pos, ext) 79 sortedtypes.insert(pos, types[n*2]) 80 if res: 81 exts.insert(pos/2,res.groups()[0]) 82 n += 1 83 84 # rejoin filetypes 85 filetypes = '|'.join(sortedtypes) 86 dlg =wx.FileDialog(self._parent, "Save to file", "", "", filetypes, 87 wx.SAVE|wx.OVERWRITE_PROMPT|wx.CHANGE_DIR) 88 if dlg.ShowModal() == wx.ID_OK: 89 dirname = dlg.GetDirectory() 90 filename = dlg.GetFilename() 91 i = dlg.GetFilterIndex() 92 dotext = '.' + exts[i] 93 if os.path.splitext(filename)[-1] != dotext: 94 filename = filename + dotext 95 # matplotlib does not like UTF strings 96 fullpath = str(os.path.join(dirname, filename)) 97 self.canvas.print_figure(fullpath) 98 return
99 100 # End class ExtendedToolbar 101
102 -class ExtendedPlotFrame(wx.Frame):
103 """An extended plotting frame with a save and close button. 104 105 The class has a matplotlib.figure.Figure data member named 'figure'. 106 It also has a matplotlib.axes.Axes data member named 'axes'. 107 The normal matplotlib plot manipulations can be performed with these two 108 data members. See the matplotlib API at: 109 http://matplotlib.sourceforge.net/classdocs.html 110 """ 111
112 - def __init__(self, parent = None, *args, **kwargs):
113 """Initialize the CanvasFrame. 114 115 The frame uses ExtendedToolbar as a toolbar, which has a save data 116 button and a close button on the toolbar in addition to the normal 117 buttons. 118 119 args -- argument list 120 kwargs -- keyword argument list 121 """ 122 wx.Frame.__init__(self,parent,-1,'ExtendedPlotFrame',size=(550,350)) 123 124 # figsize in inches 125 self.figure = Figure(figsize=(0.5,0.5), dpi=72) 126 127 # we will manage view scale ourselves 128 self.subplot = self.figure.add_subplot(111, autoscale_on=False) 129 self.canvas = FigureCanvas(self, -1, self.figure) 130 131 # Introspection data 132 self.dirname = '' 133 self.filename = '' 134 135 self.sizer = wx.BoxSizer(wx.VERTICAL) 136 self.sizer.Add(self.canvas, 1, wx.TOP|wx.LEFT|wx.EXPAND) 137 self.toolbar = ExtendedToolbar(self.canvas, True) 138 self.toolbar.Realize() 139 140 self.coordLabel = wx.StaticText(self,-1,style = wx.ALIGN_RIGHT|wx.NO_BORDER) 141 if wx.Platform == '__WXMAC__': 142 # Mac platform (OSX 10.3, MacPython) does not seem to cope with 143 # having a toolbar in a sizer. This work-around gets the buttons 144 # back, but at the expense of having the toolbar at the top 145 self.SetToolBar(self.toolbar) 146 self.sizer.Add(self.coordLabel, 0, wx.EXPAND) 147 else: 148 # On Windows platform, default window size is incorrect, so set 149 # toolbar width to figure width. 150 tw, th = self.toolbar.GetSizeTuple() 151 sw, sh = self.coordLabel.GetSizeTuple() 152 fw, fh = self.canvas.GetSizeTuple() 153 # By adding toolbar in sizer, we are able to put it at the bottom 154 # of the frame - so appearance is closer to GTK version. 155 # As noted above, doesn't work for Mac. 156 self.coordLabel.SetSize(wx.Size(sw, th)) 157 #self.coordLabel.SetBackgroundColour(self.toolbar.GetBackgroundColour()) 158 barSizer = wx.BoxSizer(wx.HORIZONTAL) 159 self.sizer.Add(barSizer, 0, wx.EXPAND|wx.CENTER) 160 barSizer.Add(self.toolbar, 0, wx.CENTER) 161 barSizer.Add((20,10),0) 162 barSizer.Add(self.coordLabel, 0, wx.CENTER) 163 164 # update the axes menu on the toolbar 165 self.toolbar.update() 166 self.SetSizer(self.sizer) 167 self.Fit() 168 self.SetSize((600,400)) 169 #Use toolbar's color for label. 170 self.SetBackgroundColour(self.toolbar.GetBackgroundColour()) 171 self.canvas.mpl_connect('motion_notify_event', self.UpdateStatusBar) 172 wx.EVT_PAINT(self, self.OnPaint) 173 wx.EVT_TOOL(self, DATA_SAVE_ID, self.savePlotData) 174 wx.EVT_TOOL(self, wx.ID_CLOSE, self.onClose) 175 wx.EVT_CLOSE(self, self.onClose) 176 wx.EVT_TOOL(self, wx.ID_PRINT, self.onPrint) 177 wx.EVT_TOOL(self, wx.ID_PRINT_SETUP, self.onPrintSetup) 178 wx.EVT_TOOL(self, wx.ID_PREVIEW_PRINT, self.onPrintPreview) 179 180 self.datalims = {}
181 182 # CUSTOM METHODS ######################################################## 183 184 # EVENT CODE #############################################################
185 - def onClose(self, evt):
186 """Close the frame.""" 187 if hasattr(self, 'plotter'): 188 self.plotter.onWindowClose() 189 self.Destroy() 190 return
191
192 - def OnPaint(self, event):
193 self.canvas.draw() 194 event.Skip()
195
196 - def savePlotData(self, evt):
197 """Save the data in the plot in columns.""" 198 d = wx.FileDialog(None, "Save as...", self.dirname, self.filename, 199 "(*.dat)|*.dat|(*.txt)|*.txt|(*)|*", wx.SAVE|wx.OVERWRITE_PROMPT) 200 if d.ShowModal() == wx.ID_OK: 201 fullname = d.GetPath() 202 self.dirname = os.path.dirname(fullname) 203 self.filename = os.path.basename(fullname) 204 self.plotter.export(fullname) 205 206 d.Destroy() 207 return
208
209 - def onPrint(self, evt):
210 '''handle print event''' 211 self.canvas.Printer_Print(event=evt)
212
213 - def onPrintSetup(self,event=None):
214 self.canvas.Printer_Setup(event=event)
215
216 - def onPrintPreview(self,event=None):
217 self.canvas.Printer_Preview(event=event)
218
219 - def UpdateStatusBar(self, event):
220 if event.inaxes: 221 x, y = event.xdata, event.ydata 222 xystr = "x = %g, y = %g" % (x, y) 223 self.coordLabel.SetLabel(xystr)
224
225 - def replot(self):
226 """officially call function in matplotlib to do drawing 227 """ 228 self.canvas.draw()
229
230 - def insertCurve(self, xData, yData, style):
231 """insert a new curve to the plot 232 233 xData, yData -- x, y data to used for the curve 234 style -- the way curve should be plotted 235 return: internal reference to the newly added curve 236 """ 237 stylestr,properties = self.__translateStyles(style) 238 curveRef = self.subplot.plot(xData, yData, stylestr, **properties)[0] 239 self.subplot.legend(**legendBoxProperties()) 240 try: 241 self.datalims[curveRef] = (min(xData), max(xData), min(yData), max(yData)) 242 except ValueError: 243 self.datalims[curveRef] = (0, 0, 0, 0) 244 self.__updateViewLimits() 245 return curveRef
246
247 - def updateData(self, curveRef, xData, yData):
248 """update data for a existing curve 249 250 curveRef -- internal reference to a curve 251 xData, yData -- x, y data to used for the curve 252 """ 253 curveRef.set_data(xData, yData) 254 try: 255 self.datalims[curveRef] = (min(xData), max(xData), min(yData), max(yData)) 256 except ValueError: 257 self.datalims[curveRef] = (0, 0, 0, 0) 258 self.__updateViewLimits()
259
260 - def changeStyle(self, curveRef, style):
261 """change curve style 262 263 curveRef -- internal reference to curves 264 style -- style dictionary 265 """ 266 stylestr, properties = self.__translateStyles(style) 267 #FIXME: we discard stylestr because it seems there's no way 268 # it can be changed afterwards. 269 setp((curveRef,), **properties) 270 self.subplot.legend(**legendBoxProperties())
271
272 - def removeCurve(self, curveRef):
273 """remove curve from plot 274 275 curveRef -- internal reference to curves 276 """ 277 del self.datalims[curveRef] 278 self.figure.gca().lines.remove(curveRef) 279 self.subplot.legend(**legendBoxProperties()) 280 self.__updateViewLimits()
281
282 - def __updateViewLimits(self):
283 """adjust the subplot range in order to show all curves correctly. 284 """ 285 # NOTE: 286 # we need to adjust view limits by ourselves because Matplotlib can't 287 # set the legend nicely when there are multiple curves in the plot. 288 # Beside, autoscale can not automatically respond to data change. 289 if len(self.datalims) == 0 : 290 return 291 # ignore previous range 292 self.subplot.dataLim.ignore(True) 293 bounds = self.datalims.values() 294 xmin = min([b[0] for b in bounds]) 295 xmax = max([b[1] for b in bounds]) 296 ymin = min([b[2] for b in bounds]) 297 ymax = max([b[3] for b in bounds]) 298 299 # If multiple curve, we need calculate new x limits because legend box 300 # take up some space 301 #NOTE: 3 and 0.33 is our best estimation for a good view 302 # 2007-10-25 PJ: it is better to use full plot area 303 # if len(self.datalims) > 3: 304 # # leave extra room for legend by shift the upper bound for x axis 305 # xmax += (xmax-xmin)*0.33 306 if xmax > xmin: 307 self.subplot.set_xlim(xmin, xmax) 308 if ymax > ymin: 309 self.subplot.set_ylim(ymin, ymax)
310
311 - def __translateStyles(self, style):
312 """Private function to translate general probabilities to 313 Matplotlib specific ones 314 315 style -- general curve style dictionary (defined in demoplot) 316 """ 317 #Translation dictionary 318 lineStyleDict ={'solid':'-','dash':'--','dot':':','dashDot':'-.'} 319 symbolDict ={'diamond':'d','square':'s','circle':'o', 320 'cross':'+','xCross':'x','triangle':'^'} 321 colorDict = {'blue':'b','green':'g','red':'r','cyan':'c', 322 'magenta':'m','yellow':'y','black':'k','white':'w', 323 'darkRed':'#8B0000', 'darkGreen':'#006400', 'darkCyan':'#008B8B', 324 'darkYellow':'#FFD700','darkBlue':'#00008B','darkMagenta':'#8B008B'} 325 326 properties = {} 327 328 #NOTE: matplotlib takes additional string for plotting. It's 329 # purpose is like 'with' in Gnuplot 330 stylestr = '' 331 # color is universal for either lines, points or linepoints 332 color = colorDict.get(style['color'], 'k') 333 334 if style['with'] in ('points', 'linespoints'): 335 # require symbol properties 336 stylestr = '.' 337 symbol = symbolDict.get(style['symbol'],'s') # prefer square 338 symbolSize = style['symbolSize'] 339 symbolColor = colorDict.get(style['symbolColor'], 'k') 340 properties.update({#'linewidth':0.0, # doesn't affect any 341 'markerfacecolor':symbolColor, 342 'markeredgecolor':color, 343 'marker':symbol,'markersize':symbolSize}) 344 if style['with'] != 'points': 345 # not 'points', so line properties are required as well 346 lineStyle = lineStyleDict.get(style['line'],'-') #prefer solid 347 lineWidth = style['width'] 348 stylestr += lineStyle 349 properties.update({'color':color,'linestyle':lineStyle, 350 'linewidth':lineWidth}) 351 352 if style.has_key('legend'): 353 properties['label'] = style['legend'] 354 return stylestr, properties
355 356
357 - def setTitle(self, wt, gt):
358 """set graph labels 359 360 wt -- window title 361 gt -- graph title 362 """ 363 self.SetTitle(wt) 364 self.figure.gca().set_title(gt)
365
366 - def setXLabel(self, x):
367 """set label for x axis 368 369 x -- x label 370 """ 371 self.figure.gca().set_xlabel(x)
372
373 - def setYLabel(self, y):
374 """set label for y axis 375 376 y -- y label 377 """ 378 self.figure.gca().set_ylabel(y)
379
380 - def clear(self):
381 """erase all curves""" 382 self.subplot.clear() 383 self.curverefs =[] 384 self.replot()
385 386 # End class ExtendedPlotFrame 387 388
389 -def legendBoxProperties():
390 """Legend properties dictionary with keys consistent with MPL version. 391 392 The argument names have changed in matplotlib 0.98.5. 393 Old arguments do not work with later versions of matplotlib. 394 395 Return dictionary of legend properties. 396 """ 397 global _lbp 398 # return immediately if properties have already been cached 399 if len(_lbp) > 0: return _lbp 400 # figure out matplotlib version and appropriate names 401 from pkg_resources import parse_version 402 from matplotlib import __version__ as mplver 403 if parse_version(mplver) >= parse_version('0.98.5'): 404 _lbp = { 405 'loc' : 'upper right', 406 'numpoints' : 3, # number of points in the legend line 407 'borderpad' : 0.25, # whitespace in the legend border 408 'labelspacing' : 0, # space between legend entries 409 'handlelength' : 1.5, # the length of the legend lines 410 'handletextpad' : 0.5, # separation between line and text 411 'prop' : FontProperties(size='medium'), 412 } 413 else: 414 _lbp = { 415 'loc' : 'upper right', 416 'numpoints' : 3, # number of points in the legend line 417 'pad' : 0.20, # whitespace in the legend border 418 'labelsep' : 0.005, # space between legend entries 419 'handlelen' : 0.03, # the length of the legend lines 420 'handletextsep' : 0.02, # separation between line and text 421 'prop' : FontProperties(size='medium'), 422 } 423 return _lbp
424 425 _lbp = {} 426 427 # End of legendBoxProperties 428 429 430 if __name__ == "__main__": 431
432 - class MyApp(wx.App):
433 - def OnInit(self):
434 from matplotlib.numerix import arange, sin, pi, cos 435 'Create the main window and insert the custom frame' 436 x = arange(0.0,3.0,0.01) 437 s = sin(2*pi*x) 438 c = cos(2*pi*x) 439 t = sin(2*pi*x) + cos(2*pi*x) 440 frame = ExtendedPlotFrame(None) 441 style = {'with':'lines', 'color':'blue','line':'solid','width':2} 442 style['legend'] = 'sin(x)' 443 frame.insertCurve(x,s, style) 444 style = {'with':'lines', 'color':'red','line':'solid','width':2} 445 style['legend'] = 'cos(x)' 446 frame.insertCurve(x,c, style) 447 style = {'with':'lines', 'color':'black','line':'solid','width':2} 448 #style['legend'] = 'sin(x)+cos(x)' 449 frame.insertCurve(x,t, style) 450 frame.Show(True) 451 return True
452 453 app = MyApp(0) 454 app.MainLoop() 455 456 457 # End of file 458