Dir rename
[pyplotsuite.git] / plotfile2.py
blob9908607fb0a968b3f365efa87a1f06d154e2989c
1 #!/usr/bin/env python
2 # -*- coding: UTF8 -*-
4 # PlotFile2 -- A tool to make quick plots from data series.
5 # Version: 0.1-alpha (see README)
7 # Copyright (C) 2007 Antonio Ingargiola <tritemio@gmail.com>
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 import sys
15 rootdir = sys.path[0] + '/'
16 sourcedir = rootdir + 'plotfile2/'
18 import os.path
19 import gtk
20 from common.gtk_generic_windows import \
21 GenericMainPlotWindow, DialogWindowWithCancel,\
22 GenericOpenFileDialog, GenericSecondaryWindow
23 from common.plotfile import xyfromfile
24 from numpy import arange, array
26 # XML GUI Description file
27 gladefile = sourcedir + 'plotfile2.glade'
29 # Minimum default axis value in log scale when the original value was <= 0
30 LOGMIN = 1e-3
32 # How many centimeter is an inch
33 CM_PER_INCHES = 2.54
35 class DataSet:
36 """An object describing data series with a shared X axis."""
37 def __init__(self, x=None, y1=None):
38 # self.x is the X axis shared for all y series
39 self.x = x
40 self.len = len(x)
42 # self.y is a list of series of data
43 self.y = []
44 if y1 != None: self.add(y1)
46 # Set the plot style, maker and linestyle attributes
47 # Marker and linestyle are separate variables so we can switch them off
48 # and then recover the style information when we turn them on
49 self.marker = '.'
50 self.linestyle = '-'
51 self.plot_kwargs = dict(
52 linestyle = self.linestyle,
53 linewidth = 1.5,
54 marker = self.marker,
55 alpha = 1
58 def add(self, yi):
59 if len(yi) != self.len:
60 raise IndexError, 'Y data length (%d) must be == len(x) (%d)'\
61 % (len(yi), self.len)
62 else:
63 self.y.append(yi)
65 class AxesProperty:
66 """Simple struct to store any properties that has different values for X
67 and Y axes"""
68 x = None
69 y = None
72 # Main window class
74 class PlotFileApp(GenericMainPlotWindow):
75 """
76 This class implements an interactive plot windows with various options.
77 """
78 def __init__(self, x=None, y=None, title='Title', debug=False):
79 GenericMainPlotWindow.__init__(self, 'PlotFileWindow', gladefile)
81 self.set_defaults()
82 self.title = title
83 self.debug = debug
85 self.setup_gui_widgets()
87 # Data: self.data is a list of DataSet() instances
88 if x != None:
89 self.data.append( DataSet(x, y) )
91 # Try to load the file passed as parameter
92 if x == None and y == None:
93 try:
94 self.load(sys.argv[1])
95 except:
96 pass
97 self.plot()
99 def set_defaults(self):
100 # Clear data
101 self.data = []
103 # Plot Defaults
104 self.title = 'Title'
105 self.xlabel = 'X Axis'
106 self.ylabel = 'Y Axis'
107 self.xscale = 'linear'
108 self.yscale = 'linear'
109 self.showPoints = True
110 self.showLines = True
111 self.grid = True
113 # Reset ranges
114 self.xmin, self.xmax, self.ymin, self.ymax = None, None, None, None
115 self.axes_limits = AxesProperty()
116 self.axes_limits.x = dict(linear=None, log=None)
117 self.axes_limits.y = dict(linear=None, log=None)
119 def reset_scale_buttons(self):
120 self.setup = True
121 if self.xscale == 'log':
122 self.xscaleCheckB.set_active(True)
123 else:
124 self.xscaleCheckB.set_active(False)
125 if self.yscale == 'log':
126 self.yscaleCheckB.set_active(True)
127 else:
128 self.yscaleCheckB.set_active(False)
129 self.setup = False
131 def setup_gui_widgets(self):
132 # Create the text entry handler
133 self.xminEntry = self.widgetTree.get_widget('xmin_entry')
134 self.xmaxEntry = self.widgetTree.get_widget('xmax_entry')
135 self.yminEntry = self.widgetTree.get_widget('ymin_entry')
136 self.ymaxEntry = self.widgetTree.get_widget('ymax_entry')
138 # Initialize the check buttons to the correct value
139 self.xscaleCheckB = self.widgetTree.get_widget('xlog_chk_butt')
140 self.yscaleCheckB = self.widgetTree.get_widget('ylog_chk_butt')
141 self.reset_scale_buttons()
143 self.cooPickerCheckB = self.widgetTree.get_widget('coordinatePicker')
144 self.moreGridCheckB = self.widgetTree.get_widget('moreGrid')
146 self.window.show_all()
147 if self.debug: print 'end of setup_gui_widgets()'
149 def is_ydata_positive(self):
150 for d in self.data:
151 for yi in d.y:
152 if (yi <= 0).any(): return False
153 return True
155 def plot(self):
156 if self.debug: print 'plot method'
158 if self.yscale == 'log' and not self.is_ydata_positive():
159 self.writeStatusBar('WARNING: Negative Y values in log scale.')
161 self.axis.clear()
163 self.axis.set_yscale('linear')
164 self.axis.set_xscale('linear')
166 for d in self.data:
167 print 'data X:', d.x
168 for yi in d.y:
169 print 'data Y:', yi
170 self.axis.plot(d.x, yi, **d.plot_kwargs)
172 self.axis.set_title(self.title)
173 self.axis.set_xlabel(self.xlabel)
174 self.axis.set_ylabel(self.ylabel)
175 self.axis.grid(self.grid)
177 print 'x', self.xscale, 'y', self.yscale
178 self.axis.set_yscale(self.yscale)
179 self.axis.set_xscale(self.xscale)
181 if len(self.data) > 0:
182 self.set_xlim()
183 if len(self.data[0].y) > 0:
184 self.set_ylim()
186 self.canvas.draw()
188 def set_xlim(self):
189 if self.axes_limits.x[self.xscale] == None:
190 if self.debug: print 'autoscaling...'
191 self.axis.autoscale_view(scalex=True, scaley=False)
192 self.xmin, self.xmax = self.axis.get_xlim()
193 else:
194 if self.debug: print 'using old axis limit', self.axes_limits.x
195 self.xmin, self.xmax = self.axes_limits.x[self.xscale]
196 if self.xscale == 'log':
197 if self.xmin <= 0: self.xmin = LOGMIN
198 if self.xmax <= self.xmin: self.xmax = self.xmin+1
199 self.axis.set_xlim(xmin=self.xmin, xmax=self.xmax)
201 if self.debug: print 'xmin:', self.xmin, 'xmax:', self.xmax
202 self.xminEntry.set_text(str(self.xmin))
203 self.xmaxEntry.set_text(str(self.xmax))
205 def set_ylim(self):
206 if self.axes_limits.y[self.yscale] == None:
207 if self.debug: print 'autoscaling...'
208 self.axis.autoscale_view(scaley=True, scalex=False)
209 self.ymin, self.ymax = self.axis.get_ylim()
210 else:
211 if self.debug: print 'using old axis limit'
212 self.ymin, self.ymax = self.axes_limits.y[self.yscale]
213 if self.yscale == 'log':
214 if self.ymin <= 0: self.ymin = LOGMIN
215 if self.ymax <= self.ymin: self.ymax = self.ymin+1
216 self.axis.set_ylim(ymin=self.ymin, ymax=self.ymax)
218 if self.debug: print 'ymin:', self.ymin, 'ymax:', self.ymax
219 self.yminEntry.set_text(str(self.ymin))
220 self.ymaxEntry.set_text(str(self.ymax))
222 def load(self, filename):
223 try:
224 x, y = xyfromfile(filename, returnarray=True)
225 except IOError:
226 self.writeStatusBar("Can't open file '%s'" % filename)
227 except:
228 self.writeStatusBar("File '%s': file format unknown." % filename)
229 else:
230 self.data.append(DataSet(x,y))
231 self.title = os.path.basename(filename)
232 self.axes_limits.x = dict(linear=None, log=None)
233 self.axes_limits.y = dict(linear=None, log=None)
236 # The following are menu callback methods
238 def on_title_and_axes_labels_activate(self, widget, *args):
239 TitleAndAxesWindow(self)
242 # The following are GUI callback methods
244 def on_xscale_toggled(self, widget, *args):
245 if not self.setup:
246 self.axes_limits.x[self.xscale] = self.axis.set_xlim()
247 if self.xscaleCheckB.get_active(): self.xscale = 'log'
248 else: self.xscale = 'linear'
249 if self.debug: print "XScale:", self.xscale
250 self.axis.set_xscale(self.xscale)
251 self.set_xlim()
252 self.canvas.draw()
254 def on_yscale_toggled(self, widget, *args):
255 if not self.setup:
256 self.axes_limits.y[self.yscale] = self.axis.set_ylim()
257 if self.yscaleCheckB.get_active(): self.yscale = 'log'
258 else: self.yscale = 'linear'
259 if self.debug: print "YScale:", self.yscale
260 self.axis.set_yscale(self.yscale)
261 self.set_ylim()
262 self.canvas.draw()
264 def on_points_toggled(self, widget, *args):
265 self.showPoints = not self.showPoints
266 if self.debug: print "Show Points:", self.showPoints
267 for d in self.data:
268 if self.showPoints:
269 # Restore the previous marker style
270 d.plot_kwargs['marker'] = d.marker
271 else:
272 # Turn off the marker visualization
273 d.plot_kwargs['marker'] = ''
274 self.plot()
276 def on_lines_toggled(self, widget, *args):
277 self.showLines = not self.showLines
278 if self.debug: print "Show Lines:", self.showLines
279 for d in self.data:
280 if self.showLines:
281 # Restore the previous linestyle
282 d.plot_kwargs['linestyle'] = d.linestyle
283 else:
284 # Turn off the line visualization
285 d.plot_kwargs['linestyle'] = ''
286 self.plot()
288 def on_grid_toggled(self, widget, *args):
289 self.grid = not self.grid
290 if self.debug: print "Show Grid:", self.grid
291 self.axis.grid(self.grid)
292 self.canvas.draw()
294 def on_xmin_entry_activate(self, widget, *args):
295 if self.debug: print 'X Min Entry Activated'
296 s = self.xminEntry.get_text()
297 try:
298 val = float(s)
299 except ValueError:
300 self.statusBar.push(self.context, 'Wrong X axis min limit')
301 else:
302 if self.xscale == 'log' and val <= 0: val = LOGMIN
303 self.xmin = val
304 self.axis.set_xlim(xmin=val)
305 self.canvas.draw()
307 def on_xmax_entry_activate(self, widget, *args):
308 if self.debug: print 'X Max Entry Activated'
309 s = self.xmaxEntry.get_text()
310 try:
311 val = float(s)
312 except ValueError:
313 self.statusBar.push(self.context, 'Wrong X axis max limit')
314 else:
315 if self.xscale == 'log' and val <= 0: val = self.xmin*10.0
316 self.xmax = val
317 self.axis.set_xlim(xmax=val)
318 self.canvas.draw()
320 def on_ymin_entry_activate(self, widget, *args):
321 if self.debug: print 'Y Min Entry Activated'
322 s = self.yminEntry.get_text()
323 try:
324 val = float(s)
325 except ValueError:
326 self.statusBar.push(self.context, 'Wrong Y axis min limit')
327 else:
328 if self.yscale == 'log' and val <= 0: val = LOGMIN
329 self.ymin = val
330 self.axis.set_ylim(ymin=val)
331 self.canvas.draw()
333 def on_ymax_entry_activate(self, widget, *args):
334 if self.debug: print 'Y Max Entry Activated'
335 s = self.ymaxEntry.get_text()
336 try:
337 val = float(s)
338 except ValueError:
339 self.statusBar.push(self.context, 'Wrong Y axis max limit')
340 else:
341 if self.yscale == 'log' and val <= 0: val = self.ymin*10.0
342 self.ymax = val
343 self.axis.set_ylim(ymax=val)
344 self.canvas.draw()
346 def on_apply_button_clicked(self, widget, *args):
347 sxmin = self.xminEntry.get_text()
348 sxmax = self.xmaxEntry.get_text()
349 symin = self.yminEntry.get_text()
350 symax = self.ymaxEntry.get_text()
351 try:
352 xmin_val = float(sxmin)
353 xmax_val = float(sxmax)
354 ymin_val = float(symin)
355 ymax_val = float(symax)
356 except ValueError:
357 self.statusBar.push(self.context, 'Wrong axis limit')
358 else:
359 if self.xscale == 'log':
360 if xmin_val <= 0: xmin_val = LOGMIN
361 if xmax_val <= 0: xmax_val = xmin_val*10
362 if ymin_val <= 0: ymin_val = LOGMIN
363 if ymax_val <= 0: ymax_val = ymin_val*10
364 self.xmin, self.xmax, self.ymin, self.ymax = \
365 xmin_val, xmax_val, ymin_val, ymax_val
366 self.axis.set_xlim(xmin=xmin_val)
367 self.axis.set_xlim(xmax=xmax_val)
368 self.axis.set_ylim(ymin=ymin_val)
369 self.axis.set_ylim(ymax=ymax_val)
370 self.canvas.draw()
373 # The following are MENU callback methods
375 def on_addDataSet_activate(self, widget, *args):
376 OpenFileDialog(self)
377 def on_new_activate(self, widget, *args):
378 self.set_defaults()
379 self.reset_scale_buttons()
380 self.plot()
381 def on_dimensionsResolution_activate(self, widget, *args):
382 DimentionAndResolution(self)
383 def on_autoscale_activate(self, widget, *args):
384 self.axis.autoscale_view()
385 self.canvas.draw()
386 def on_coordinatePicker_activate(self, widget, *args):
387 if self.cooPickerCheckB.get_active():
388 self.cid = self.canvas.mpl_connect('button_press_event',
389 get_coordinates_cb)
390 self.writeStatusBar('Click on the plot to log coordinates on '+\
391 'the terminal.')
392 elif self.cid != None:
393 self.canvas.mpl_disconnect(self.cid)
394 self.writeStatusBar('Coordinate picker disabled.')
395 else:
396 print "Error: tried to disconnect an unexistent event."
397 def on_moreGrid_activate(self, widget, *args):
398 if self.moreGridCheckB.get_active():
399 self.axis.xaxis.grid(True, which='minor', ls='--', alpha=0.5)
400 self.axis.yaxis.grid(True, which='minor', ls='--', alpha=0.5)
401 self.axis.xaxis.grid(True, which='major', ls='-', alpha=0.5)
402 self.axis.yaxis.grid(True, which='major', ls='-', alpha=0.5)
403 else:
404 self.axis.xaxis.grid(False, which='minor')
405 self.axis.yaxis.grid(False, which='minor')
406 self.axis.xaxis.grid(True, which='major', ls=':')
407 self.axis.yaxis.grid(True, which='major', ls=':')
408 self.canvas.draw()
409 def on_info_activate(self, widget, *args):
410 AboutDialog()
411 def on_plot_properties_activate(self, widget, *args):
412 PlotPropertiesDialog(self)
414 def get_coordinates_cb(event):
416 Callback for the coordinate picker tool. This function is not a class
417 method because I don't know how to connect an MPL event to a method instead
418 of a function.
420 if not event.inaxes: return
421 print "%3.4f\t%3.4f" % (event.xdata, event.ydata)
424 # Dialogs classes
426 class PlotPropertiesDialog(DialogWindowWithCancel):
427 def __init__(self, callerApp):
428 DialogWindowWithCancel.__init__(self, 'PlotPropertiesDialog',
429 gladefile, callerApp)
430 self.treeView = self.widgetTree.get_widget('treeview')
432 # Add a column to the treeView
433 self.addListColumn('Plot List', 0)
435 # Create the listStore Model to use with the wineView
436 self.plotList = gtk.ListStore(str)
438 # Populate the list
439 self.plotList.append(row=['antonio0'])
440 #self.insertPlotList(self.plotList)
442 # Attatch the model to the treeView
443 self.treeView.set_model(self.plotList)
445 def addListColumn(self, title, columnId):
446 """This function adds a column to the list view.
447 First it create the gtk.TreeViewColumn and then set
448 some needed properties"""
450 column = gtk.TreeViewColumn(title, gtk.CellRendererText())
451 column.set_resizable(True)
452 column.set_sort_column_id(columnId)
453 self.treeView.append_column(column)
455 def insertPlotList(self, listStore):
456 self.plotList.append(row=['antonio0'])
457 self.plotList.append(row=['antonio1'])
458 self.plotList.append(row=['antonio2'])
459 print self.plotList
460 print len( self.plotList), 'LLLLLLLLLL'
461 print [x[0] for x in self.plotList]
462 return
463 for i in range(len(self.callerApp.data)):
464 self.plotList.append('Plot %d' % i)
467 class TitleAndAxesWindow(DialogWindowWithCancel):
469 Dialog for setting Title and axes labels.
471 def __init__(self, callerApp):
472 DialogWindowWithCancel.__init__(self, 'TitleAndAxesWindow', gladefile,
473 callerApp)
475 self.titleEntry = self.widgetTree.get_widget('titleEntry')
476 self.xlabelEntry = self.widgetTree.get_widget('xlabelEntry')
477 self.ylabelEntry = self.widgetTree.get_widget('ylabelEntry')
479 def sync(field, entry):
480 if field != None: entry.set_text(field)
482 sync(self.callerApp.title, self.titleEntry)
483 sync(self.callerApp.xlabel, self.xlabelEntry)
484 sync(self.callerApp.ylabel, self.ylabelEntry)
486 def on_okButton_clicked(self, widget, *args):
487 print "OK Clicked"
488 self.callerApp.axis.set_title(self.titleEntry.get_text())
489 self.callerApp.axis.set_xlabel(self.xlabelEntry.get_text())
490 self.callerApp.axis.set_ylabel(self.ylabelEntry.get_text())
491 self.callerApp.canvas.draw()
493 self.callerApp.title = self.titleEntry.get_text()
494 self.callerApp.xlabel = self.ylabelEntry.get_text()
495 self.callerApp.ylabel = self.xlabelEntry.get_text()
497 self.window.destroy()
499 class DimentionAndResolution(DialogWindowWithCancel):
501 Dialog for setting Figure dimentions and resolution.
503 def __init__(self, callerApp):
504 DialogWindowWithCancel.__init__(self, 'DimentionsDialog', gladefile,
505 callerApp)
506 self.xdimSpinButton = self.widgetTree.get_widget('xdimSpinButton')
507 self.ydimSpinButton = self.widgetTree.get_widget('ydimSpinButton')
508 self.resolutionSpinButton = self.widgetTree.get_widget(
509 'resolutionSpinButton')
510 self.inchesRadioB = self.widgetTree.get_widget('inRadioButton')
512 self.xdim_o, self.ydim_o = self.callerApp.figure.get_size_inches()
513 self.dpi_o = self.callerApp.figure.get_dpi()
515 self.set_initial_values()
517 def set_values(self, xdim, ydim, dpi):
518 if not self.inchesRadioB.get_active():
519 xdim *= CM_PER_INCHES
520 ydim *= CM_PER_INCHES
521 self.xdimSpinButton.set_value(xdim)
522 self.ydimSpinButton.set_value(ydim)
523 self.resolutionSpinButton.set_value(dpi)
525 def set_initial_values(self):
526 self.set_values(self.xdim_o, self.ydim_o, self.dpi_o)
528 def set_default_values(self):
529 xdim, ydim = rcParams['figure.figsize']
530 dpi = rcParams['figure.dpi']
531 self.set_values(xdim, ydim, dpi)
533 def get_values(self):
534 self.xdim = self.xdimSpinButton.get_value()
535 self.ydim = self.ydimSpinButton.get_value()
536 self.resolution = self.resolutionSpinButton.get_value()
537 if not self.inchesRadioB.get_active():
538 self.xdim /= CM_PER_INCHES
539 self.ydim /= CM_PER_INCHES
541 def set_figsize(self):
542 self.callerApp.figure.set_size_inches(self.xdim, self.ydim)
543 self.callerApp.figure.set_dpi(self.resolution)
545 def on_unity_toggled(self, widget, *args):
546 xdim = self.xdimSpinButton.get_value()
547 ydim = self.ydimSpinButton.get_value()
548 if self.inchesRadioB.get_active():
549 xdim /= CM_PER_INCHES
550 ydim /= CM_PER_INCHES
551 else:
552 xdim *= CM_PER_INCHES
553 ydim *= CM_PER_INCHES
554 self.xdimSpinButton.set_value(xdim)
555 self.ydimSpinButton.set_value(ydim)
557 def on_okButton_clicked(self, widget, *args):
558 print "OK"
559 self.get_values()
560 self.set_figsize()
561 self.callerApp.canvas.draw()
562 self.window.destroy()
564 def on_restoreButton_clicked(self, widget, *args):
565 self.set_default_values()
567 class OpenFileDialog(GenericOpenFileDialog):
569 This class implements the "Open File" dialog.
571 def __init__(self, callerApp):
572 GenericOpenFileDialog.__init__(self, 'OpenFileDialog', gladefile,
573 callerApp)
575 def openSelectedFile(self):
576 self.callerApp.load( self.filename )
577 self.callerApp.plot()
578 self.window.destroy()
580 class AboutDialog(GenericSecondaryWindow):
582 Object for the "About Dialog".
584 def __init__(self):
585 GenericSecondaryWindow.__init__(self, 'AboutDialog', gladefile)
586 self.window.set_version(read_version())
589 # Functions
591 def read_version():
592 """ Extract version from README file. """
593 file = open(sourcedir+'README')
594 ver = None
595 for line in file:
596 if line.strip().startswith('Version:'):
597 s = line.split()
598 ver = s[1]
599 break
600 file.close()
601 return ver
603 def plot_from_file(fname):
604 try:
605 datafile = open(fname)
606 except:
607 pass
609 def test():
610 x = arange(100)
611 y = sin(x/10.0)
612 p = PlotFileApp(x, y, title='Random Sequence', debug=True)
613 p.start()
615 def main():
616 p = PlotFileApp(debug=True)
617 # Try to open a sample file
618 if len(sys.argv) == 1 and os.path.isfile(sourcedir+'data.txt'):
619 p.load(sourcedir+'data.txt')
620 p.plot()
621 p.start()
623 if __name__ == '__main__': main()