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 # self.yline is a list of plotted lines from the current DataSet
50 # Set the plot style, maker and linestyle attributes
51 # Marker and linestyle are separate variables so we can switch them off
52 # and then recover the style information when we turn them on
55 self
.plot_kwargs
= dict(
56 linestyle
= self
.linestyle
,
63 if len(yi
) != self
.len:
64 raise IndexError, 'Y data length (%d) must be == len(x) (%d)'\
70 """Simple struct to store any properties that has different values for X
78 class PlotFileApp(GenericMainPlotWindow
):
80 This class implements an interactive plot windows with various options.
82 def __init__(self
, x
=None, y
=None, title
='Title', debug
=False):
83 GenericMainPlotWindow
.__init
__(self
, 'PlotFileWindow', gladefile
)
89 self
.setup_gui_widgets()
91 # Data: self.data is a list of DataSet() instances
93 self
.data
.append( DataSet(x
, y
, 'Plot 1') )
95 # Try to load the file passed as parameter
96 if x
== None and y
== None:
98 self
.load(sys
.argv
[1])
103 def set_defaults(self
):
109 self
.xlabel
= 'X Axis'
110 self
.ylabel
= 'Y Axis'
111 self
.xscale
= 'linear'
112 self
.yscale
= 'linear'
113 self
.showPoints
= True
114 self
.showLines
= True
118 self
.xmin
, self
.xmax
, self
.ymin
, self
.ymax
= None, None, None, None
119 self
.axes_limits
= AxesProperty()
120 self
.axes_limits
.x
= dict(linear
=None, log
=None)
121 self
.axes_limits
.y
= dict(linear
=None, log
=None)
123 def reset_scale_buttons(self
):
125 if self
.xscale
== 'log':
126 self
.xscaleCheckB
.set_active(True)
128 self
.xscaleCheckB
.set_active(False)
129 if self
.yscale
== 'log':
130 self
.yscaleCheckB
.set_active(True)
132 self
.yscaleCheckB
.set_active(False)
135 def setup_gui_widgets(self
):
136 # Create the text entry handler
137 self
.xminEntry
= self
.widgetTree
.get_widget('xmin_entry')
138 self
.xmaxEntry
= self
.widgetTree
.get_widget('xmax_entry')
139 self
.yminEntry
= self
.widgetTree
.get_widget('ymin_entry')
140 self
.ymaxEntry
= self
.widgetTree
.get_widget('ymax_entry')
142 # Initialize the check buttons to the correct value
143 self
.xscaleCheckB
= self
.widgetTree
.get_widget('xlog_chk_butt')
144 self
.yscaleCheckB
= self
.widgetTree
.get_widget('ylog_chk_butt')
145 self
.reset_scale_buttons()
147 self
.cooPickerCheckB
= self
.widgetTree
.get_widget('coordinatePicker')
148 self
.moreGridCheckB
= self
.widgetTree
.get_widget('moreGrid')
150 self
.window
.show_all()
151 if self
.debug
: print 'end of setup_gui_widgets()'
153 def is_ydata_positive(self
):
156 if (yi
<= 0).any(): return False
160 if self
.debug
: print 'plot method'
162 if self
.yscale
== 'log' and not self
.is_ydata_positive():
163 self
.writeStatusBar('WARNING: Negative Y values in log scale.')
167 self
.axis
.set_yscale('linear')
168 self
.axis
.set_xscale('linear')
173 l
+= [ self
.axis
.plot(d
.x
, yi
, **d
.plot_kwargs
) ]
175 if len(d
.yline
) == 0:
178 self
.axis
.set_title(self
.title
)
179 self
.axis
.set_xlabel(self
.xlabel
)
180 self
.axis
.set_ylabel(self
.ylabel
)
181 self
.axis
.grid(self
.grid
)
183 print 'x', self
.xscale
, 'y', self
.yscale
184 self
.axis
.set_yscale(self
.yscale
)
185 self
.axis
.set_xscale(self
.xscale
)
187 if len(self
.data
) > 0:
189 if len(self
.data
[0].y
) > 0:
195 if self
.axes_limits
.x
[self
.xscale
] == None:
196 if self
.debug
: print 'autoscaling...'
197 self
.axis
.autoscale_view(scalex
=True, scaley
=False)
198 self
.xmin
, self
.xmax
= self
.axis
.get_xlim()
200 if self
.debug
: print 'using old axis limit', self
.axes_limits
.x
201 self
.xmin
, self
.xmax
= self
.axes_limits
.x
[self
.xscale
]
202 if self
.xscale
== 'log':
203 if self
.xmin
<= 0: self
.xmin
= LOGMIN
204 if self
.xmax
<= self
.xmin
: self
.xmax
= self
.xmin
+1
205 self
.axis
.set_xlim(xmin
=self
.xmin
, xmax
=self
.xmax
)
207 if self
.debug
: print 'xmin:', self
.xmin
, 'xmax:', self
.xmax
208 self
.xminEntry
.set_text(str(self
.xmin
))
209 self
.xmaxEntry
.set_text(str(self
.xmax
))
212 if self
.axes_limits
.y
[self
.yscale
] == None:
213 if self
.debug
: print 'autoscaling...'
214 self
.axis
.autoscale_view(scaley
=True, scalex
=False)
215 self
.ymin
, self
.ymax
= self
.axis
.get_ylim()
217 if self
.debug
: print 'using old axis limit'
218 self
.ymin
, self
.ymax
= self
.axes_limits
.y
[self
.yscale
]
219 if self
.yscale
== 'log':
220 if self
.ymin
<= 0: self
.ymin
= LOGMIN
221 if self
.ymax
<= self
.ymin
: self
.ymax
= self
.ymin
+1
222 self
.axis
.set_ylim(ymin
=self
.ymin
, ymax
=self
.ymax
)
224 if self
.debug
: print 'ymin:', self
.ymin
, 'ymax:', self
.ymax
225 self
.yminEntry
.set_text(str(self
.ymin
))
226 self
.ymaxEntry
.set_text(str(self
.ymax
))
228 def load(self
, filename
):
230 x
, y
= xyfromfile(filename
, returnarray
=True)
232 self
.writeStatusBar("Can't open file '%s'" % filename
)
234 self
.writeStatusBar("File '%s': file format unknown." % filename
)
236 filename
= os
.path
.basename(filename
)
237 self
.data
.append(DataSet(x
,y
, filename
))
238 self
.title
= filename
239 self
.axes_limits
.x
= dict(linear
=None, log
=None)
240 self
.axes_limits
.y
= dict(linear
=None, log
=None)
243 # The following are menu callback methods
245 def on_title_and_axes_labels_activate(self
, widget
, *args
):
246 TitleAndAxesWindow(self
)
249 # The following are GUI callback methods
251 def on_xscale_toggled(self
, widget
, *args
):
253 self
.axes_limits
.x
[self
.xscale
] = self
.axis
.set_xlim()
254 if self
.xscaleCheckB
.get_active(): self
.xscale
= 'log'
255 else: self
.xscale
= 'linear'
256 if self
.debug
: print "XScale:", self
.xscale
257 self
.axis
.set_xscale(self
.xscale
)
261 def on_yscale_toggled(self
, widget
, *args
):
263 self
.axes_limits
.y
[self
.yscale
] = self
.axis
.set_ylim()
264 if self
.yscaleCheckB
.get_active(): self
.yscale
= 'log'
265 else: self
.yscale
= 'linear'
266 if self
.debug
: print "YScale:", self
.yscale
267 self
.axis
.set_yscale(self
.yscale
)
271 def on_points_toggled(self
, widget
, *args
):
272 self
.showPoints
= not self
.showPoints
273 if self
.debug
: print "Show Points:", self
.showPoints
276 # Restore the previous marker style
277 d
.plot_kwargs
['marker'] = d
.marker
279 # Turn off the marker visualization
280 d
.plot_kwargs
['marker'] = ''
283 def on_lines_toggled(self
, widget
, *args
):
284 self
.showLines
= not self
.showLines
285 if self
.debug
: print "Show Lines:", self
.showLines
288 # Restore the previous linestyle
289 d
.plot_kwargs
['linestyle'] = d
.linestyle
291 # Turn off the line visualization
292 d
.plot_kwargs
['linestyle'] = ''
295 def on_grid_toggled(self
, widget
, *args
):
296 self
.grid
= not self
.grid
297 if self
.debug
: print "Show Grid:", self
.grid
298 self
.axis
.grid(self
.grid
)
301 def on_xmin_entry_activate(self
, widget
, *args
):
302 if self
.debug
: print 'X Min Entry Activated'
303 s
= self
.xminEntry
.get_text()
307 self
.statusBar
.push(self
.context
, 'Wrong X axis min limit')
309 if self
.xscale
== 'log' and val
<= 0: val
= LOGMIN
311 self
.axis
.set_xlim(xmin
=val
)
314 def on_xmax_entry_activate(self
, widget
, *args
):
315 if self
.debug
: print 'X Max Entry Activated'
316 s
= self
.xmaxEntry
.get_text()
320 self
.statusBar
.push(self
.context
, 'Wrong X axis max limit')
322 if self
.xscale
== 'log' and val
<= 0: val
= self
.xmin
*10.0
324 self
.axis
.set_xlim(xmax
=val
)
327 def on_ymin_entry_activate(self
, widget
, *args
):
328 if self
.debug
: print 'Y Min Entry Activated'
329 s
= self
.yminEntry
.get_text()
333 self
.statusBar
.push(self
.context
, 'Wrong Y axis min limit')
335 if self
.yscale
== 'log' and val
<= 0: val
= LOGMIN
337 self
.axis
.set_ylim(ymin
=val
)
340 def on_ymax_entry_activate(self
, widget
, *args
):
341 if self
.debug
: print 'Y Max Entry Activated'
342 s
= self
.ymaxEntry
.get_text()
346 self
.statusBar
.push(self
.context
, 'Wrong Y axis max limit')
348 if self
.yscale
== 'log' and val
<= 0: val
= self
.ymin
*10.0
350 self
.axis
.set_ylim(ymax
=val
)
353 def on_apply_button_clicked(self
, widget
, *args
):
354 sxmin
= self
.xminEntry
.get_text()
355 sxmax
= self
.xmaxEntry
.get_text()
356 symin
= self
.yminEntry
.get_text()
357 symax
= self
.ymaxEntry
.get_text()
359 xmin_val
= float(sxmin
)
360 xmax_val
= float(sxmax
)
361 ymin_val
= float(symin
)
362 ymax_val
= float(symax
)
364 self
.statusBar
.push(self
.context
, 'Wrong axis limit')
366 if self
.xscale
== 'log':
367 if xmin_val
<= 0: xmin_val
= LOGMIN
368 if xmax_val
<= 0: xmax_val
= xmin_val
*10
369 if ymin_val
<= 0: ymin_val
= LOGMIN
370 if ymax_val
<= 0: ymax_val
= ymin_val
*10
371 self
.xmin
, self
.xmax
, self
.ymin
, self
.ymax
= \
372 xmin_val
, xmax_val
, ymin_val
, ymax_val
373 self
.axis
.set_xlim(xmin
=xmin_val
)
374 self
.axis
.set_xlim(xmax
=xmax_val
)
375 self
.axis
.set_ylim(ymin
=ymin_val
)
376 self
.axis
.set_ylim(ymax
=ymax_val
)
380 # The following are MENU callback methods
382 def on_addDataSet_activate(self
, widget
, *args
):
384 def on_new_activate(self
, widget
, *args
):
386 self
.reset_scale_buttons()
388 def on_dimensionsResolution_activate(self
, widget
, *args
):
389 DimentionAndResolution(self
)
390 def on_autoscale_activate(self
, widget
, *args
):
391 self
.axis
.autoscale_view()
393 def on_coordinatePicker_activate(self
, widget
, *args
):
394 if self
.cooPickerCheckB
.get_active():
395 self
.cid
= self
.canvas
.mpl_connect('button_press_event',
397 self
.writeStatusBar('Click on the plot to log coordinates on '+\
399 elif self
.cid
!= None:
400 self
.canvas
.mpl_disconnect(self
.cid
)
401 self
.writeStatusBar('Coordinate picker disabled.')
403 print "Error: tried to disconnect an unexistent event."
404 def on_moreGrid_activate(self
, widget
, *args
):
405 if self
.moreGridCheckB
.get_active():
406 self
.axis
.xaxis
.grid(True, which
='minor', ls
='--', alpha
=0.5)
407 self
.axis
.yaxis
.grid(True, which
='minor', ls
='--', alpha
=0.5)
408 self
.axis
.xaxis
.grid(True, which
='major', ls
='-', alpha
=0.5)
409 self
.axis
.yaxis
.grid(True, which
='major', ls
='-', alpha
=0.5)
411 self
.axis
.xaxis
.grid(False, which
='minor')
412 self
.axis
.yaxis
.grid(False, which
='minor')
413 self
.axis
.xaxis
.grid(True, which
='major', ls
=':')
414 self
.axis
.yaxis
.grid(True, which
='major', ls
=':')
416 def on_info_activate(self
, widget
, *args
):
418 def on_plot_properties_activate(self
, widget
, *args
):
419 PlotPropertiesDialog(self
)
421 def get_coordinates_cb(event
):
423 Callback for the coordinate picker tool. This function is not a class
424 method because I don't know how to connect an MPL event to a method instead
427 if not event
.inaxes
: return
428 print "%3.4f\t%3.4f" % (event
.xdata
, event
.ydata
)
433 class PlotPropertiesDialog(DialogWindowWithCancel
):
434 def __init__(self
, callerApp
):
435 DialogWindowWithCancel
.__init
__(self
, 'PlotPropertiesDialog',
436 gladefile
, callerApp
)
438 self
.treeView
= self
.widgetTree
.get_widget('treeview')
439 self
.create_plotlist()
442 self
.plot_index
= None
444 #self.markerTypeModel = self.markerTypeComboB.get_model()
446 def load_widgets(self
):
447 self
.lineWidthSpinButt
= \
448 self
.widgetTree
.get_widget('lineWidthSpinButton')
449 self
.lineStyleComboB
= \
450 self
.widgetTree
.get_widget('lineStyleComboBox')
451 self
.lineColorComboB
= \
452 self
.widgetTree
.get_widget('lineColorComboBox')
453 self
.markerTypeComboB
= \
454 self
.widgetTree
.get_widget('markerTypeComboBox')
455 self
.markerSizeSpinButt
= \
456 self
.widgetTree
.get_widget('markerSizeSpinButton')
457 self
.markerEdgeColorComboB
= \
458 self
.widgetTree
.get_widget('markerEdgeColComboBox')
459 self
.markerFaceColorComboB
= \
460 self
.widgetTree
.get_widget('markerFaceColComboBox')
462 def create_plotlist(self
):
463 # Add a column to the treeView
464 self
.addListColumn('Plot List', 0)
466 # Create the listStore Model to use with the treeView
467 self
.plotList
= gtk
.ListStore(str)
470 self
.insertPlotList(self
.plotList
)
472 # Attatch the model to the treeView
473 self
.treeView
.set_model(self
.plotList
)
475 def addListColumn(self
, title
, columnId
):
476 """This function adds a column to the list view.
477 First it create the gtk.TreeViewColumn and then set
478 some needed properties"""
480 column
= gtk
.TreeViewColumn(title
, gtk
.CellRendererText(), text
=0)
481 column
.set_resizable(True)
482 column
.set_sort_column_id(columnId
)
483 self
.treeView
.append_column(column
)
485 def insertPlotList(self
, listStore
):
486 for data
in self
.callerApp
.data
:
487 self
.plotList
.append([data
.name
])
492 def on_treeview_cursor_changed(self
, *args
):
493 self
.plot_index
= self
.treeView
.get_cursor()[0][0]
494 print self
.plot_index
495 data
= self
.callerApp
.data
[self
.plot_index
]
496 #self.markerTypeComboB.
498 def on_lineWidth_value_changed(self
, *args
):
499 lw
= self
.lineWidthSpinButt
.get_value()
500 if self
.plot_index
is not None:
501 self
.callerApp
.data
[self
.plot_index
].plot_kwargs
['linewidth'] = lw
502 self
.callerApp
.plot()
504 def on_lineStyle_changed(self
, *args
):
505 text
= self
.lineStyleComboB
.get_active_text()
508 def on_lineColor_changed(self
, *args
):
509 text
= self
.lineColorComboB
.get_active_text()
512 def on_markerType_changed(self
, *args
):
513 text
= self
.markerTypeComboB
.get_active_text()
516 def on_markerSize_value_changed(self
, *args
):
517 text
= self
.markerSizeSpinButt
.get_value()
520 def on_markerEdgeColor_changed(self
, *args
):
521 text
= self
.markerEdgeColorComboB
.get_active_text()
524 def on_markerFaceColor_changed(self
, *args
):
525 text
= self
.markerFaceColorComboB
.get_active_text()
528 def on_okButton_clicked(self
, widget
, *args
):
529 self
.window
.destroy()
531 class TitleAndAxesWindow(DialogWindowWithCancel
):
533 Dialog for setting Title and axes labels.
535 def __init__(self
, callerApp
):
536 DialogWindowWithCancel
.__init
__(self
, 'TitleAndAxesWindow', gladefile
,
539 self
.titleEntry
= self
.widgetTree
.get_widget('titleEntry')
540 self
.xlabelEntry
= self
.widgetTree
.get_widget('xlabelEntry')
541 self
.ylabelEntry
= self
.widgetTree
.get_widget('ylabelEntry')
543 def sync(field
, entry
):
544 if field
!= None: entry
.set_text(field
)
546 sync(self
.callerApp
.title
, self
.titleEntry
)
547 sync(self
.callerApp
.xlabel
, self
.xlabelEntry
)
548 sync(self
.callerApp
.ylabel
, self
.ylabelEntry
)
550 def on_okButton_clicked(self
, widget
, *args
):
552 self
.callerApp
.axis
.set_title(self
.titleEntry
.get_text())
553 self
.callerApp
.axis
.set_xlabel(self
.xlabelEntry
.get_text())
554 self
.callerApp
.axis
.set_ylabel(self
.ylabelEntry
.get_text())
555 self
.callerApp
.canvas
.draw()
557 self
.callerApp
.title
= self
.titleEntry
.get_text()
558 self
.callerApp
.xlabel
= self
.ylabelEntry
.get_text()
559 self
.callerApp
.ylabel
= self
.xlabelEntry
.get_text()
561 self
.window
.destroy()
563 class DimentionAndResolution(DialogWindowWithCancel
):
565 Dialog for setting Figure dimentions and resolution.
567 def __init__(self
, callerApp
):
568 DialogWindowWithCancel
.__init
__(self
, 'DimentionsDialog', gladefile
,
570 self
.xdimSpinButton
= self
.widgetTree
.get_widget('xdimSpinButton')
571 self
.ydimSpinButton
= self
.widgetTree
.get_widget('ydimSpinButton')
572 self
.resolutionSpinButton
= self
.widgetTree
.get_widget(
573 'resolutionSpinButton')
574 self
.inchesRadioB
= self
.widgetTree
.get_widget('inRadioButton')
576 self
.xdim_o
, self
.ydim_o
= self
.callerApp
.figure
.get_size_inches()
577 self
.dpi_o
= self
.callerApp
.figure
.get_dpi()
579 self
.set_initial_values()
581 def set_values(self
, xdim
, ydim
, dpi
):
582 if not self
.inchesRadioB
.get_active():
583 xdim
*= CM_PER_INCHES
584 ydim
*= CM_PER_INCHES
585 self
.xdimSpinButton
.set_value(xdim
)
586 self
.ydimSpinButton
.set_value(ydim
)
587 self
.resolutionSpinButton
.set_value(dpi
)
589 def set_initial_values(self
):
590 self
.set_values(self
.xdim_o
, self
.ydim_o
, self
.dpi_o
)
592 def set_default_values(self
):
593 xdim
, ydim
= rcParams
['figure.figsize']
594 dpi
= rcParams
['figure.dpi']
595 self
.set_values(xdim
, ydim
, dpi
)
597 def get_values(self
):
598 self
.xdim
= self
.xdimSpinButton
.get_value()
599 self
.ydim
= self
.ydimSpinButton
.get_value()
600 self
.resolution
= self
.resolutionSpinButton
.get_value()
601 if not self
.inchesRadioB
.get_active():
602 self
.xdim
/= CM_PER_INCHES
603 self
.ydim
/= CM_PER_INCHES
605 def set_figsize(self
):
606 self
.callerApp
.figure
.set_size_inches(self
.xdim
, self
.ydim
)
607 self
.callerApp
.figure
.set_dpi(self
.resolution
)
609 def on_unity_toggled(self
, widget
, *args
):
610 xdim
= self
.xdimSpinButton
.get_value()
611 ydim
= self
.ydimSpinButton
.get_value()
612 if self
.inchesRadioB
.get_active():
613 xdim
/= CM_PER_INCHES
614 ydim
/= CM_PER_INCHES
616 xdim
*= CM_PER_INCHES
617 ydim
*= CM_PER_INCHES
618 self
.xdimSpinButton
.set_value(xdim
)
619 self
.ydimSpinButton
.set_value(ydim
)
621 def on_okButton_clicked(self
, widget
, *args
):
625 self
.callerApp
.canvas
.draw()
626 self
.window
.destroy()
628 def on_restoreButton_clicked(self
, widget
, *args
):
629 self
.set_default_values()
631 class OpenFileDialog(GenericOpenFileDialog
):
633 This class implements the "Open File" dialog.
635 def __init__(self
, callerApp
):
636 GenericOpenFileDialog
.__init
__(self
, 'OpenFileDialog', gladefile
,
639 def openSelectedFile(self
):
640 self
.callerApp
.load( self
.filename
)
641 self
.callerApp
.plot()
642 self
.window
.destroy()
644 class AboutDialog(GenericSecondaryWindow
):
646 Object for the "About Dialog".
649 GenericSecondaryWindow
.__init
__(self
, 'AboutDialog', gladefile
)
650 self
.window
.set_version(read_version())
656 """ Extract version from README file. """
657 file = open(rootdir
+'README')
660 if line
.strip().startswith('*Latest Version*'):
667 def plot_from_file(fname
):
669 datafile
= open(fname
)
676 p
= PlotFileApp(x
, y
, title
='Random Sequence', debug
=True)
680 p
= PlotFileApp(debug
=True)
681 # Try to open a sample file
682 if len(sys
.argv
) < 2 and os
.path
.isfile(sourcedir
+'samples/data.txt'):
683 p
.load(sourcedir
+'samples/data.txt')
687 if __name__
== '__main__': main()