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.
15 rootdir
= sys
.path
[0] + '/'
16 sourcedir
= rootdir
+ 'plotfile/'
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
32 # How many centimeter is an inch
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
42 # self.y is a list of series of data
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
51 self
.plot_kwargs
= dict(
52 linestyle
= self
.linestyle
,
59 if len(yi
) != self
.len:
60 raise IndexError, 'Y data length (%d) must be == len(x) (%d)'\
66 """Simple struct to store any properties that has different values for X
74 class PlotFileApp(GenericMainPlotWindow
):
76 This class implements an interactive plot windows with various options.
78 def __init__(self
, x
=None, y
=None, title
='Title', debug
=False):
79 GenericMainPlotWindow
.__init
__(self
, 'PlotFileWindow', gladefile
)
85 self
.setup_gui_widgets()
87 # Data: self.data is a list of DataSet() instances
89 self
.data
.append( DataSet(x
, y
) )
91 # Try to load the file passed as parameter
92 if x
== None and y
== None:
94 self
.load(sys
.argv
[1])
99 def set_defaults(self
):
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
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
):
121 if self
.xscale
== 'log':
122 self
.xscaleCheckB
.set_active(True)
124 self
.xscaleCheckB
.set_active(False)
125 if self
.yscale
== 'log':
126 self
.yscaleCheckB
.set_active(True)
128 self
.yscaleCheckB
.set_active(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
):
152 if (yi
<= 0).any(): return False
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.')
163 self
.axis
.set_yscale('linear')
164 self
.axis
.set_xscale('linear')
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:
183 if len(self
.data
[0].y
) > 0:
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()
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
))
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()
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
):
224 x
, y
= xyfromfile(filename
, returnarray
=True)
226 self
.writeStatusBar("Can't open file '%s'" % filename
)
228 self
.writeStatusBar("File '%s': file format unknown." % filename
)
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
):
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
)
254 def on_yscale_toggled(self
, widget
, *args
):
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
)
264 def on_points_toggled(self
, widget
, *args
):
265 self
.showPoints
= not self
.showPoints
266 if self
.debug
: print "Show Points:", self
.showPoints
269 # Restore the previous marker style
270 d
.plot_kwargs
['marker'] = d
.marker
272 # Turn off the marker visualization
273 d
.plot_kwargs
['marker'] = ''
276 def on_lines_toggled(self
, widget
, *args
):
277 self
.showLines
= not self
.showLines
278 if self
.debug
: print "Show Lines:", self
.showLines
281 # Restore the previous linestyle
282 d
.plot_kwargs
['linestyle'] = d
.linestyle
284 # Turn off the line visualization
285 d
.plot_kwargs
['linestyle'] = ''
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
)
294 def on_xmin_entry_activate(self
, widget
, *args
):
295 if self
.debug
: print 'X Min Entry Activated'
296 s
= self
.xminEntry
.get_text()
300 self
.statusBar
.push(self
.context
, 'Wrong X axis min limit')
302 if self
.xscale
== 'log' and val
<= 0: val
= LOGMIN
304 self
.axis
.set_xlim(xmin
=val
)
307 def on_xmax_entry_activate(self
, widget
, *args
):
308 if self
.debug
: print 'X Max Entry Activated'
309 s
= self
.xmaxEntry
.get_text()
313 self
.statusBar
.push(self
.context
, 'Wrong X axis max limit')
315 if self
.xscale
== 'log' and val
<= 0: val
= self
.xmin
*10.0
317 self
.axis
.set_xlim(xmax
=val
)
320 def on_ymin_entry_activate(self
, widget
, *args
):
321 if self
.debug
: print 'Y Min Entry Activated'
322 s
= self
.yminEntry
.get_text()
326 self
.statusBar
.push(self
.context
, 'Wrong Y axis min limit')
328 if self
.yscale
== 'log' and val
<= 0: val
= LOGMIN
330 self
.axis
.set_ylim(ymin
=val
)
333 def on_ymax_entry_activate(self
, widget
, *args
):
334 if self
.debug
: print 'Y Max Entry Activated'
335 s
= self
.ymaxEntry
.get_text()
339 self
.statusBar
.push(self
.context
, 'Wrong Y axis max limit')
341 if self
.yscale
== 'log' and val
<= 0: val
= self
.ymin
*10.0
343 self
.axis
.set_ylim(ymax
=val
)
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()
352 xmin_val
= float(sxmin
)
353 xmax_val
= float(sxmax
)
354 ymin_val
= float(symin
)
355 ymax_val
= float(symax
)
357 self
.statusBar
.push(self
.context
, 'Wrong axis limit')
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
)
373 # The following are MENU callback methods
375 def on_addDataSet_activate(self
, widget
, *args
):
377 def on_new_activate(self
, widget
, *args
):
379 self
.reset_scale_buttons()
381 def on_dimensionsResolution_activate(self
, widget
, *args
):
382 DimentionAndResolution(self
)
383 def on_autoscale_activate(self
, widget
, *args
):
384 self
.axis
.autoscale_view()
386 def on_coordinatePicker_activate(self
, widget
, *args
):
387 if self
.cooPickerCheckB
.get_active():
388 self
.cid
= self
.canvas
.mpl_connect('button_press_event',
390 self
.writeStatusBar('Click on the plot to log coordinates on '+\
392 elif self
.cid
!= None:
393 self
.canvas
.mpl_disconnect(self
.cid
)
394 self
.writeStatusBar('Coordinate picker disabled.')
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)
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
=':')
409 def on_info_activate(self
, widget
, *args
):
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
420 if not event
.inaxes
: return
421 print "%3.4f\t%3.4f" % (event
.xdata
, event
.ydata
)
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)
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'])
460 print len( self
.plotList
), 'LLLLLLLLLL'
461 print [x
[0] for x
in self
.plotList
]
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
,
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
):
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
,
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
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
):
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
,
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".
585 GenericSecondaryWindow
.__init
__(self
, 'AboutDialog', gladefile
)
586 self
.window
.set_version(read_version())
592 """ Extract version from README file. """
593 file = open(rootdir
+'README')
596 if line
.strip().startswith('*Latest Version*'):
603 def plot_from_file(fname
):
605 datafile
= open(fname
)
612 p
= PlotFileApp(x
, y
, title
='Random Sequence', debug
=True)
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')
623 if __name__
== '__main__': main()