Retrive the plot index on click in plot-properties.
[pyplotsuite.git] / plotfile2.py
blob2ab1cd7c7c0dd92a737f2725ccb6342d2e8bc817
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 + 'plotfile/'
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, name=None):
38 # self.x is the X axis shared for all y series
39 self.x = x
40 self.len = len(x)
41 self.name = name
43 # self.y is a list of series of data
44 self.y = []
45 if y1 != None: self.add(y1)
47 # Set the plot style, maker and linestyle attributes
48 # Marker and linestyle are separate variables so we can switch them off
49 # and then recover the style information when we turn them on
50 self.marker = '.'
51 self.linestyle = '-'
52 self.plot_kwargs = dict(
53 linestyle = self.linestyle,
54 linewidth = 1.5,
55 marker = self.marker,
56 alpha = 1
59 def add(self, yi):
60 if len(yi) != self.len:
61 raise IndexError, 'Y data length (%d) must be == len(x) (%d)'\
62 % (len(yi), self.len)
63 else:
64 self.y.append(yi)
66 class AxesProperty:
67 """Simple struct to store any properties that has different values for X
68 and Y axes"""
69 x = None
70 y = None
73 # Main window class
75 class PlotFileApp(GenericMainPlotWindow):
76 """
77 This class implements an interactive plot windows with various options.
78 """
79 def __init__(self, x=None, y=None, title='Title', debug=False):
80 GenericMainPlotWindow.__init__(self, 'PlotFileWindow', gladefile)
82 self.set_defaults()
83 self.title = title
84 self.debug = debug
86 self.setup_gui_widgets()
88 # Data: self.data is a list of DataSet() instances
89 if x != None:
90 self.data.append( DataSet(x, y, 'Plot 1') )
92 # Try to load the file passed as parameter
93 if x == None and y == None:
94 try:
95 self.load(sys.argv[1])
96 except:
97 pass
98 self.plot()
100 def set_defaults(self):
101 # Clear data
102 self.data = []
104 # Plot Defaults
105 self.title = 'Title'
106 self.xlabel = 'X Axis'
107 self.ylabel = 'Y Axis'
108 self.xscale = 'linear'
109 self.yscale = 'linear'
110 self.showPoints = True
111 self.showLines = True
112 self.grid = True
114 # Reset ranges
115 self.xmin, self.xmax, self.ymin, self.ymax = None, None, None, None
116 self.axes_limits = AxesProperty()
117 self.axes_limits.x = dict(linear=None, log=None)
118 self.axes_limits.y = dict(linear=None, log=None)
120 def reset_scale_buttons(self):
121 self.setup = True
122 if self.xscale == 'log':
123 self.xscaleCheckB.set_active(True)
124 else:
125 self.xscaleCheckB.set_active(False)
126 if self.yscale == 'log':
127 self.yscaleCheckB.set_active(True)
128 else:
129 self.yscaleCheckB.set_active(False)
130 self.setup = False
132 def setup_gui_widgets(self):
133 # Create the text entry handler
134 self.xminEntry = self.widgetTree.get_widget('xmin_entry')
135 self.xmaxEntry = self.widgetTree.get_widget('xmax_entry')
136 self.yminEntry = self.widgetTree.get_widget('ymin_entry')
137 self.ymaxEntry = self.widgetTree.get_widget('ymax_entry')
139 # Initialize the check buttons to the correct value
140 self.xscaleCheckB = self.widgetTree.get_widget('xlog_chk_butt')
141 self.yscaleCheckB = self.widgetTree.get_widget('ylog_chk_butt')
142 self.reset_scale_buttons()
144 self.cooPickerCheckB = self.widgetTree.get_widget('coordinatePicker')
145 self.moreGridCheckB = self.widgetTree.get_widget('moreGrid')
147 self.window.show_all()
148 if self.debug: print 'end of setup_gui_widgets()'
150 def is_ydata_positive(self):
151 for d in self.data:
152 for yi in d.y:
153 if (yi <= 0).any(): return False
154 return True
156 def plot(self):
157 if self.debug: print 'plot method'
159 if self.yscale == 'log' and not self.is_ydata_positive():
160 self.writeStatusBar('WARNING: Negative Y values in log scale.')
162 self.axis.clear()
164 self.axis.set_yscale('linear')
165 self.axis.set_xscale('linear')
167 for d in self.data:
168 print 'data X:', d.x
169 for yi in d.y:
170 print 'data Y:', yi
171 self.axis.plot(d.x, yi, **d.plot_kwargs)
173 self.axis.set_title(self.title)
174 self.axis.set_xlabel(self.xlabel)
175 self.axis.set_ylabel(self.ylabel)
176 self.axis.grid(self.grid)
178 print 'x', self.xscale, 'y', self.yscale
179 self.axis.set_yscale(self.yscale)
180 self.axis.set_xscale(self.xscale)
182 if len(self.data) > 0:
183 self.set_xlim()
184 if len(self.data[0].y) > 0:
185 self.set_ylim()
187 self.canvas.draw()
189 def set_xlim(self):
190 if self.axes_limits.x[self.xscale] == None:
191 if self.debug: print 'autoscaling...'
192 self.axis.autoscale_view(scalex=True, scaley=False)
193 self.xmin, self.xmax = self.axis.get_xlim()
194 else:
195 if self.debug: print 'using old axis limit', self.axes_limits.x
196 self.xmin, self.xmax = self.axes_limits.x[self.xscale]
197 if self.xscale == 'log':
198 if self.xmin <= 0: self.xmin = LOGMIN
199 if self.xmax <= self.xmin: self.xmax = self.xmin+1
200 self.axis.set_xlim(xmin=self.xmin, xmax=self.xmax)
202 if self.debug: print 'xmin:', self.xmin, 'xmax:', self.xmax
203 self.xminEntry.set_text(str(self.xmin))
204 self.xmaxEntry.set_text(str(self.xmax))
206 def set_ylim(self):
207 if self.axes_limits.y[self.yscale] == None:
208 if self.debug: print 'autoscaling...'
209 self.axis.autoscale_view(scaley=True, scalex=False)
210 self.ymin, self.ymax = self.axis.get_ylim()
211 else:
212 if self.debug: print 'using old axis limit'
213 self.ymin, self.ymax = self.axes_limits.y[self.yscale]
214 if self.yscale == 'log':
215 if self.ymin <= 0: self.ymin = LOGMIN
216 if self.ymax <= self.ymin: self.ymax = self.ymin+1
217 self.axis.set_ylim(ymin=self.ymin, ymax=self.ymax)
219 if self.debug: print 'ymin:', self.ymin, 'ymax:', self.ymax
220 self.yminEntry.set_text(str(self.ymin))
221 self.ymaxEntry.set_text(str(self.ymax))
223 def load(self, filename):
224 try:
225 x, y = xyfromfile(filename, returnarray=True)
226 except IOError:
227 self.writeStatusBar("Can't open file '%s'" % filename)
228 except:
229 self.writeStatusBar("File '%s': file format unknown." % filename)
230 else:
231 filename = os.path.basename(filename)
232 self.data.append(DataSet(x,y, filename))
233 self.title = filename
234 self.axes_limits.x = dict(linear=None, log=None)
235 self.axes_limits.y = dict(linear=None, log=None)
238 # The following are menu callback methods
240 def on_title_and_axes_labels_activate(self, widget, *args):
241 TitleAndAxesWindow(self)
244 # The following are GUI callback methods
246 def on_xscale_toggled(self, widget, *args):
247 if not self.setup:
248 self.axes_limits.x[self.xscale] = self.axis.set_xlim()
249 if self.xscaleCheckB.get_active(): self.xscale = 'log'
250 else: self.xscale = 'linear'
251 if self.debug: print "XScale:", self.xscale
252 self.axis.set_xscale(self.xscale)
253 self.set_xlim()
254 self.canvas.draw()
256 def on_yscale_toggled(self, widget, *args):
257 if not self.setup:
258 self.axes_limits.y[self.yscale] = self.axis.set_ylim()
259 if self.yscaleCheckB.get_active(): self.yscale = 'log'
260 else: self.yscale = 'linear'
261 if self.debug: print "YScale:", self.yscale
262 self.axis.set_yscale(self.yscale)
263 self.set_ylim()
264 self.canvas.draw()
266 def on_points_toggled(self, widget, *args):
267 self.showPoints = not self.showPoints
268 if self.debug: print "Show Points:", self.showPoints
269 for d in self.data:
270 if self.showPoints:
271 # Restore the previous marker style
272 d.plot_kwargs['marker'] = d.marker
273 else:
274 # Turn off the marker visualization
275 d.plot_kwargs['marker'] = ''
276 self.plot()
278 def on_lines_toggled(self, widget, *args):
279 self.showLines = not self.showLines
280 if self.debug: print "Show Lines:", self.showLines
281 for d in self.data:
282 if self.showLines:
283 # Restore the previous linestyle
284 d.plot_kwargs['linestyle'] = d.linestyle
285 else:
286 # Turn off the line visualization
287 d.plot_kwargs['linestyle'] = ''
288 self.plot()
290 def on_grid_toggled(self, widget, *args):
291 self.grid = not self.grid
292 if self.debug: print "Show Grid:", self.grid
293 self.axis.grid(self.grid)
294 self.canvas.draw()
296 def on_xmin_entry_activate(self, widget, *args):
297 if self.debug: print 'X Min Entry Activated'
298 s = self.xminEntry.get_text()
299 try:
300 val = float(s)
301 except ValueError:
302 self.statusBar.push(self.context, 'Wrong X axis min limit')
303 else:
304 if self.xscale == 'log' and val <= 0: val = LOGMIN
305 self.xmin = val
306 self.axis.set_xlim(xmin=val)
307 self.canvas.draw()
309 def on_xmax_entry_activate(self, widget, *args):
310 if self.debug: print 'X Max Entry Activated'
311 s = self.xmaxEntry.get_text()
312 try:
313 val = float(s)
314 except ValueError:
315 self.statusBar.push(self.context, 'Wrong X axis max limit')
316 else:
317 if self.xscale == 'log' and val <= 0: val = self.xmin*10.0
318 self.xmax = val
319 self.axis.set_xlim(xmax=val)
320 self.canvas.draw()
322 def on_ymin_entry_activate(self, widget, *args):
323 if self.debug: print 'Y Min Entry Activated'
324 s = self.yminEntry.get_text()
325 try:
326 val = float(s)
327 except ValueError:
328 self.statusBar.push(self.context, 'Wrong Y axis min limit')
329 else:
330 if self.yscale == 'log' and val <= 0: val = LOGMIN
331 self.ymin = val
332 self.axis.set_ylim(ymin=val)
333 self.canvas.draw()
335 def on_ymax_entry_activate(self, widget, *args):
336 if self.debug: print 'Y Max Entry Activated'
337 s = self.ymaxEntry.get_text()
338 try:
339 val = float(s)
340 except ValueError:
341 self.statusBar.push(self.context, 'Wrong Y axis max limit')
342 else:
343 if self.yscale == 'log' and val <= 0: val = self.ymin*10.0
344 self.ymax = val
345 self.axis.set_ylim(ymax=val)
346 self.canvas.draw()
348 def on_apply_button_clicked(self, widget, *args):
349 sxmin = self.xminEntry.get_text()
350 sxmax = self.xmaxEntry.get_text()
351 symin = self.yminEntry.get_text()
352 symax = self.ymaxEntry.get_text()
353 try:
354 xmin_val = float(sxmin)
355 xmax_val = float(sxmax)
356 ymin_val = float(symin)
357 ymax_val = float(symax)
358 except ValueError:
359 self.statusBar.push(self.context, 'Wrong axis limit')
360 else:
361 if self.xscale == 'log':
362 if xmin_val <= 0: xmin_val = LOGMIN
363 if xmax_val <= 0: xmax_val = xmin_val*10
364 if ymin_val <= 0: ymin_val = LOGMIN
365 if ymax_val <= 0: ymax_val = ymin_val*10
366 self.xmin, self.xmax, self.ymin, self.ymax = \
367 xmin_val, xmax_val, ymin_val, ymax_val
368 self.axis.set_xlim(xmin=xmin_val)
369 self.axis.set_xlim(xmax=xmax_val)
370 self.axis.set_ylim(ymin=ymin_val)
371 self.axis.set_ylim(ymax=ymax_val)
372 self.canvas.draw()
375 # The following are MENU callback methods
377 def on_addDataSet_activate(self, widget, *args):
378 OpenFileDialog(self)
379 def on_new_activate(self, widget, *args):
380 self.set_defaults()
381 self.reset_scale_buttons()
382 self.plot()
383 def on_dimensionsResolution_activate(self, widget, *args):
384 DimentionAndResolution(self)
385 def on_autoscale_activate(self, widget, *args):
386 self.axis.autoscale_view()
387 self.canvas.draw()
388 def on_coordinatePicker_activate(self, widget, *args):
389 if self.cooPickerCheckB.get_active():
390 self.cid = self.canvas.mpl_connect('button_press_event',
391 get_coordinates_cb)
392 self.writeStatusBar('Click on the plot to log coordinates on '+\
393 'the terminal.')
394 elif self.cid != None:
395 self.canvas.mpl_disconnect(self.cid)
396 self.writeStatusBar('Coordinate picker disabled.')
397 else:
398 print "Error: tried to disconnect an unexistent event."
399 def on_moreGrid_activate(self, widget, *args):
400 if self.moreGridCheckB.get_active():
401 self.axis.xaxis.grid(True, which='minor', ls='--', alpha=0.5)
402 self.axis.yaxis.grid(True, which='minor', ls='--', alpha=0.5)
403 self.axis.xaxis.grid(True, which='major', ls='-', alpha=0.5)
404 self.axis.yaxis.grid(True, which='major', ls='-', alpha=0.5)
405 else:
406 self.axis.xaxis.grid(False, which='minor')
407 self.axis.yaxis.grid(False, which='minor')
408 self.axis.xaxis.grid(True, which='major', ls=':')
409 self.axis.yaxis.grid(True, which='major', ls=':')
410 self.canvas.draw()
411 def on_info_activate(self, widget, *args):
412 AboutDialog()
413 def on_plot_properties_activate(self, widget, *args):
414 PlotPropertiesDialog(self)
416 def get_coordinates_cb(event):
418 Callback for the coordinate picker tool. This function is not a class
419 method because I don't know how to connect an MPL event to a method instead
420 of a function.
422 if not event.inaxes: return
423 print "%3.4f\t%3.4f" % (event.xdata, event.ydata)
426 # Dialogs classes
428 class PlotPropertiesDialog(DialogWindowWithCancel):
429 def __init__(self, callerApp):
430 DialogWindowWithCancel.__init__(self, 'PlotPropertiesDialog',
431 gladefile, callerApp)
433 self.treeView = self.widgetTree.get_widget('treeview')
434 self.create_plotlist()
437 def create_plotlist(self):
438 # Add a column to the treeView
439 self.addListColumn('Plot List', 0)
441 # Create the listStore Model to use with the treeView
442 self.plotList = gtk.ListStore(str)
444 # Populate the list
445 self.insertPlotList(self.plotList)
447 # Attatch the model to the treeView
448 self.treeView.set_model(self.plotList)
450 def addListColumn(self, title, columnId):
451 """This function adds a column to the list view.
452 First it create the gtk.TreeViewColumn and then set
453 some needed properties"""
455 column = gtk.TreeViewColumn(title, gtk.CellRendererText(), text=0)
456 column.set_resizable(True)
457 column.set_sort_column_id(columnId)
458 self.treeView.append_column(column)
460 def insertPlotList(self, listStore):
461 for data in self.callerApp.data:
462 self.plotList.append([data.name])
464 def on_treeview_cursor_changed(self, *args):
465 plot_index = self.treeView.get_cursor()[0][0]
466 print plot_index
470 class TitleAndAxesWindow(DialogWindowWithCancel):
472 Dialog for setting Title and axes labels.
474 def __init__(self, callerApp):
475 DialogWindowWithCancel.__init__(self, 'TitleAndAxesWindow', gladefile,
476 callerApp)
478 self.titleEntry = self.widgetTree.get_widget('titleEntry')
479 self.xlabelEntry = self.widgetTree.get_widget('xlabelEntry')
480 self.ylabelEntry = self.widgetTree.get_widget('ylabelEntry')
482 def sync(field, entry):
483 if field != None: entry.set_text(field)
485 sync(self.callerApp.title, self.titleEntry)
486 sync(self.callerApp.xlabel, self.xlabelEntry)
487 sync(self.callerApp.ylabel, self.ylabelEntry)
489 def on_okButton_clicked(self, widget, *args):
490 print "OK Clicked"
491 self.callerApp.axis.set_title(self.titleEntry.get_text())
492 self.callerApp.axis.set_xlabel(self.xlabelEntry.get_text())
493 self.callerApp.axis.set_ylabel(self.ylabelEntry.get_text())
494 self.callerApp.canvas.draw()
496 self.callerApp.title = self.titleEntry.get_text()
497 self.callerApp.xlabel = self.ylabelEntry.get_text()
498 self.callerApp.ylabel = self.xlabelEntry.get_text()
500 self.window.destroy()
502 class DimentionAndResolution(DialogWindowWithCancel):
504 Dialog for setting Figure dimentions and resolution.
506 def __init__(self, callerApp):
507 DialogWindowWithCancel.__init__(self, 'DimentionsDialog', gladefile,
508 callerApp)
509 self.xdimSpinButton = self.widgetTree.get_widget('xdimSpinButton')
510 self.ydimSpinButton = self.widgetTree.get_widget('ydimSpinButton')
511 self.resolutionSpinButton = self.widgetTree.get_widget(
512 'resolutionSpinButton')
513 self.inchesRadioB = self.widgetTree.get_widget('inRadioButton')
515 self.xdim_o, self.ydim_o = self.callerApp.figure.get_size_inches()
516 self.dpi_o = self.callerApp.figure.get_dpi()
518 self.set_initial_values()
520 def set_values(self, xdim, ydim, dpi):
521 if not self.inchesRadioB.get_active():
522 xdim *= CM_PER_INCHES
523 ydim *= CM_PER_INCHES
524 self.xdimSpinButton.set_value(xdim)
525 self.ydimSpinButton.set_value(ydim)
526 self.resolutionSpinButton.set_value(dpi)
528 def set_initial_values(self):
529 self.set_values(self.xdim_o, self.ydim_o, self.dpi_o)
531 def set_default_values(self):
532 xdim, ydim = rcParams['figure.figsize']
533 dpi = rcParams['figure.dpi']
534 self.set_values(xdim, ydim, dpi)
536 def get_values(self):
537 self.xdim = self.xdimSpinButton.get_value()
538 self.ydim = self.ydimSpinButton.get_value()
539 self.resolution = self.resolutionSpinButton.get_value()
540 if not self.inchesRadioB.get_active():
541 self.xdim /= CM_PER_INCHES
542 self.ydim /= CM_PER_INCHES
544 def set_figsize(self):
545 self.callerApp.figure.set_size_inches(self.xdim, self.ydim)
546 self.callerApp.figure.set_dpi(self.resolution)
548 def on_unity_toggled(self, widget, *args):
549 xdim = self.xdimSpinButton.get_value()
550 ydim = self.ydimSpinButton.get_value()
551 if self.inchesRadioB.get_active():
552 xdim /= CM_PER_INCHES
553 ydim /= CM_PER_INCHES
554 else:
555 xdim *= CM_PER_INCHES
556 ydim *= CM_PER_INCHES
557 self.xdimSpinButton.set_value(xdim)
558 self.ydimSpinButton.set_value(ydim)
560 def on_okButton_clicked(self, widget, *args):
561 print "OK"
562 self.get_values()
563 self.set_figsize()
564 self.callerApp.canvas.draw()
565 self.window.destroy()
567 def on_restoreButton_clicked(self, widget, *args):
568 self.set_default_values()
570 class OpenFileDialog(GenericOpenFileDialog):
572 This class implements the "Open File" dialog.
574 def __init__(self, callerApp):
575 GenericOpenFileDialog.__init__(self, 'OpenFileDialog', gladefile,
576 callerApp)
578 def openSelectedFile(self):
579 self.callerApp.load( self.filename )
580 self.callerApp.plot()
581 self.window.destroy()
583 class AboutDialog(GenericSecondaryWindow):
585 Object for the "About Dialog".
587 def __init__(self):
588 GenericSecondaryWindow.__init__(self, 'AboutDialog', gladefile)
589 self.window.set_version(read_version())
592 # Functions
594 def read_version():
595 """ Extract version from README file. """
596 file = open(rootdir+'README')
597 ver = None
598 for line in file:
599 if line.strip().startswith('*Latest Version*'):
600 s = line.split()
601 ver = s[2]
602 break
603 file.close()
604 return ver
606 def plot_from_file(fname):
607 try:
608 datafile = open(fname)
609 except:
610 pass
612 def test():
613 x = arange(100)
614 y = sin(x/10.0)
615 p = PlotFileApp(x, y, title='Random Sequence', debug=True)
616 p.start()
618 def main():
619 p = PlotFileApp(debug=True)
620 # Try to open a sample file
621 if len(sys.argv) < 2 and os.path.isfile(sourcedir+'samples/data.txt'):
622 p.load(sourcedir+'samples/data.txt')
623 p.plot()
624 p.start()
626 if __name__ == '__main__': main()