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, name
=None):
38 # self.x is the X axis shared for all y series
43 # self.y is a list of series of data
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
52 self
.plot_kwargs
= dict(
53 linestyle
= self
.linestyle
,
60 if len(yi
) != self
.len:
61 raise IndexError, 'Y data length (%d) must be == len(x) (%d)'\
67 """Simple struct to store any properties that has different values for X
75 class PlotFileApp(GenericMainPlotWindow
):
77 This class implements an interactive plot windows with various options.
79 def __init__(self
, x
=None, y
=None, title
='Title', debug
=False):
80 GenericMainPlotWindow
.__init
__(self
, 'PlotFileWindow', gladefile
)
86 self
.setup_gui_widgets()
88 # Data: self.data is a list of DataSet() instances
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:
95 self
.load(sys
.argv
[1])
100 def set_defaults(self
):
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
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
):
122 if self
.xscale
== 'log':
123 self
.xscaleCheckB
.set_active(True)
125 self
.xscaleCheckB
.set_active(False)
126 if self
.yscale
== 'log':
127 self
.yscaleCheckB
.set_active(True)
129 self
.yscaleCheckB
.set_active(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
):
153 if (yi
<= 0).any(): return False
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.')
164 self
.axis
.set_yscale('linear')
165 self
.axis
.set_xscale('linear')
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:
184 if len(self
.data
[0].y
) > 0:
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()
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
))
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()
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
):
225 x
, y
= xyfromfile(filename
, returnarray
=True)
227 self
.writeStatusBar("Can't open file '%s'" % filename
)
229 self
.writeStatusBar("File '%s': file format unknown." % filename
)
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
):
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
)
256 def on_yscale_toggled(self
, widget
, *args
):
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
)
266 def on_points_toggled(self
, widget
, *args
):
267 self
.showPoints
= not self
.showPoints
268 if self
.debug
: print "Show Points:", self
.showPoints
271 # Restore the previous marker style
272 d
.plot_kwargs
['marker'] = d
.marker
274 # Turn off the marker visualization
275 d
.plot_kwargs
['marker'] = ''
278 def on_lines_toggled(self
, widget
, *args
):
279 self
.showLines
= not self
.showLines
280 if self
.debug
: print "Show Lines:", self
.showLines
283 # Restore the previous linestyle
284 d
.plot_kwargs
['linestyle'] = d
.linestyle
286 # Turn off the line visualization
287 d
.plot_kwargs
['linestyle'] = ''
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
)
296 def on_xmin_entry_activate(self
, widget
, *args
):
297 if self
.debug
: print 'X Min Entry Activated'
298 s
= self
.xminEntry
.get_text()
302 self
.statusBar
.push(self
.context
, 'Wrong X axis min limit')
304 if self
.xscale
== 'log' and val
<= 0: val
= LOGMIN
306 self
.axis
.set_xlim(xmin
=val
)
309 def on_xmax_entry_activate(self
, widget
, *args
):
310 if self
.debug
: print 'X Max Entry Activated'
311 s
= self
.xmaxEntry
.get_text()
315 self
.statusBar
.push(self
.context
, 'Wrong X axis max limit')
317 if self
.xscale
== 'log' and val
<= 0: val
= self
.xmin
*10.0
319 self
.axis
.set_xlim(xmax
=val
)
322 def on_ymin_entry_activate(self
, widget
, *args
):
323 if self
.debug
: print 'Y Min Entry Activated'
324 s
= self
.yminEntry
.get_text()
328 self
.statusBar
.push(self
.context
, 'Wrong Y axis min limit')
330 if self
.yscale
== 'log' and val
<= 0: val
= LOGMIN
332 self
.axis
.set_ylim(ymin
=val
)
335 def on_ymax_entry_activate(self
, widget
, *args
):
336 if self
.debug
: print 'Y Max Entry Activated'
337 s
= self
.ymaxEntry
.get_text()
341 self
.statusBar
.push(self
.context
, 'Wrong Y axis max limit')
343 if self
.yscale
== 'log' and val
<= 0: val
= self
.ymin
*10.0
345 self
.axis
.set_ylim(ymax
=val
)
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()
354 xmin_val
= float(sxmin
)
355 xmax_val
= float(sxmax
)
356 ymin_val
= float(symin
)
357 ymax_val
= float(symax
)
359 self
.statusBar
.push(self
.context
, 'Wrong axis limit')
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
)
375 # The following are MENU callback methods
377 def on_addDataSet_activate(self
, widget
, *args
):
379 def on_new_activate(self
, widget
, *args
):
381 self
.reset_scale_buttons()
383 def on_dimensionsResolution_activate(self
, widget
, *args
):
384 DimentionAndResolution(self
)
385 def on_autoscale_activate(self
, widget
, *args
):
386 self
.axis
.autoscale_view()
388 def on_coordinatePicker_activate(self
, widget
, *args
):
389 if self
.cooPickerCheckB
.get_active():
390 self
.cid
= self
.canvas
.mpl_connect('button_press_event',
392 self
.writeStatusBar('Click on the plot to log coordinates on '+\
394 elif self
.cid
!= None:
395 self
.canvas
.mpl_disconnect(self
.cid
)
396 self
.writeStatusBar('Coordinate picker disabled.')
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)
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
=':')
411 def on_info_activate(self
, widget
, *args
):
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
422 if not event
.inaxes
: return
423 print "%3.4f\t%3.4f" % (event
.xdata
, event
.ydata
)
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)
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]
470 class TitleAndAxesWindow(DialogWindowWithCancel
):
472 Dialog for setting Title and axes labels.
474 def __init__(self
, callerApp
):
475 DialogWindowWithCancel
.__init
__(self
, 'TitleAndAxesWindow', gladefile
,
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
):
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
,
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
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
):
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
,
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".
588 GenericSecondaryWindow
.__init
__(self
, 'AboutDialog', gladefile
)
589 self
.window
.set_version(read_version())
595 """ Extract version from README file. """
596 file = open(rootdir
+'README')
599 if line
.strip().startswith('*Latest Version*'):
606 def plot_from_file(fname
):
608 datafile
= open(fname
)
615 p
= PlotFileApp(x
, y
, title
='Random Sequence', debug
=True)
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')
626 if __name__
== '__main__': main()