Made Linewidth work on the Plot Properties Dialog.
[pyplotsuite.git] / plotfile2.py
blob4dc994f0fcd03594fc4a564a5471b67822bebd53
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 # self.yline is a list of plotted lines from the current DataSet
48 self.yline = []
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
53 self.marker = '.'
54 self.linestyle = '-'
55 self.plot_kwargs = dict(
56 linestyle = self.linestyle,
57 linewidth = 1.5,
58 marker = self.marker,
59 alpha = 1
62 def add(self, yi):
63 if len(yi) != self.len:
64 raise IndexError, 'Y data length (%d) must be == len(x) (%d)'\
65 % (len(yi), self.len)
66 else:
67 self.y.append(yi)
69 class AxesProperty:
70 """Simple struct to store any properties that has different values for X
71 and Y axes"""
72 x = None
73 y = None
76 # Main window class
78 class PlotFileApp(GenericMainPlotWindow):
79 """
80 This class implements an interactive plot windows with various options.
81 """
82 def __init__(self, x=None, y=None, title='Title', debug=False):
83 GenericMainPlotWindow.__init__(self, 'PlotFileWindow', gladefile)
85 self.set_defaults()
86 self.title = title
87 self.debug = debug
89 self.setup_gui_widgets()
91 # Data: self.data is a list of DataSet() instances
92 if x != None:
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:
97 try:
98 self.load(sys.argv[1])
99 except:
100 pass
101 self.plot()
103 def set_defaults(self):
104 # Clear data
105 self.data = []
107 # Plot Defaults
108 self.title = 'Title'
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
115 self.grid = True
117 # Reset ranges
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):
124 self.setup = True
125 if self.xscale == 'log':
126 self.xscaleCheckB.set_active(True)
127 else:
128 self.xscaleCheckB.set_active(False)
129 if self.yscale == 'log':
130 self.yscaleCheckB.set_active(True)
131 else:
132 self.yscaleCheckB.set_active(False)
133 self.setup = 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):
154 for d in self.data:
155 for yi in d.y:
156 if (yi <= 0).any(): return False
157 return True
159 def plot(self):
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.')
165 self.axis.clear()
167 self.axis.set_yscale('linear')
168 self.axis.set_xscale('linear')
170 for d in self.data:
171 l = []
172 for yi in d.y:
173 l += [ self.axis.plot(d.x, yi, **d.plot_kwargs) ]
175 if len(d.yline) == 0:
176 d.yline = l
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:
188 self.set_xlim()
189 if len(self.data[0].y) > 0:
190 self.set_ylim()
192 self.canvas.draw()
194 def set_xlim(self):
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()
199 else:
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))
211 def set_ylim(self):
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()
216 else:
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):
229 try:
230 x, y = xyfromfile(filename, returnarray=True)
231 except IOError:
232 self.writeStatusBar("Can't open file '%s'" % filename)
233 except:
234 self.writeStatusBar("File '%s': file format unknown." % filename)
235 else:
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):
252 if not self.setup:
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)
258 self.set_xlim()
259 self.canvas.draw()
261 def on_yscale_toggled(self, widget, *args):
262 if not self.setup:
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)
268 self.set_ylim()
269 self.canvas.draw()
271 def on_points_toggled(self, widget, *args):
272 self.showPoints = not self.showPoints
273 if self.debug: print "Show Points:", self.showPoints
274 for d in self.data:
275 if self.showPoints:
276 # Restore the previous marker style
277 d.plot_kwargs['marker'] = d.marker
278 else:
279 # Turn off the marker visualization
280 d.plot_kwargs['marker'] = ''
281 self.plot()
283 def on_lines_toggled(self, widget, *args):
284 self.showLines = not self.showLines
285 if self.debug: print "Show Lines:", self.showLines
286 for d in self.data:
287 if self.showLines:
288 # Restore the previous linestyle
289 d.plot_kwargs['linestyle'] = d.linestyle
290 else:
291 # Turn off the line visualization
292 d.plot_kwargs['linestyle'] = ''
293 self.plot()
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)
299 self.canvas.draw()
301 def on_xmin_entry_activate(self, widget, *args):
302 if self.debug: print 'X Min Entry Activated'
303 s = self.xminEntry.get_text()
304 try:
305 val = float(s)
306 except ValueError:
307 self.statusBar.push(self.context, 'Wrong X axis min limit')
308 else:
309 if self.xscale == 'log' and val <= 0: val = LOGMIN
310 self.xmin = val
311 self.axis.set_xlim(xmin=val)
312 self.canvas.draw()
314 def on_xmax_entry_activate(self, widget, *args):
315 if self.debug: print 'X Max Entry Activated'
316 s = self.xmaxEntry.get_text()
317 try:
318 val = float(s)
319 except ValueError:
320 self.statusBar.push(self.context, 'Wrong X axis max limit')
321 else:
322 if self.xscale == 'log' and val <= 0: val = self.xmin*10.0
323 self.xmax = val
324 self.axis.set_xlim(xmax=val)
325 self.canvas.draw()
327 def on_ymin_entry_activate(self, widget, *args):
328 if self.debug: print 'Y Min Entry Activated'
329 s = self.yminEntry.get_text()
330 try:
331 val = float(s)
332 except ValueError:
333 self.statusBar.push(self.context, 'Wrong Y axis min limit')
334 else:
335 if self.yscale == 'log' and val <= 0: val = LOGMIN
336 self.ymin = val
337 self.axis.set_ylim(ymin=val)
338 self.canvas.draw()
340 def on_ymax_entry_activate(self, widget, *args):
341 if self.debug: print 'Y Max Entry Activated'
342 s = self.ymaxEntry.get_text()
343 try:
344 val = float(s)
345 except ValueError:
346 self.statusBar.push(self.context, 'Wrong Y axis max limit')
347 else:
348 if self.yscale == 'log' and val <= 0: val = self.ymin*10.0
349 self.ymax = val
350 self.axis.set_ylim(ymax=val)
351 self.canvas.draw()
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()
358 try:
359 xmin_val = float(sxmin)
360 xmax_val = float(sxmax)
361 ymin_val = float(symin)
362 ymax_val = float(symax)
363 except ValueError:
364 self.statusBar.push(self.context, 'Wrong axis limit')
365 else:
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)
377 self.canvas.draw()
380 # The following are MENU callback methods
382 def on_addDataSet_activate(self, widget, *args):
383 OpenFileDialog(self)
384 def on_new_activate(self, widget, *args):
385 self.set_defaults()
386 self.reset_scale_buttons()
387 self.plot()
388 def on_dimensionsResolution_activate(self, widget, *args):
389 DimentionAndResolution(self)
390 def on_autoscale_activate(self, widget, *args):
391 self.axis.autoscale_view()
392 self.canvas.draw()
393 def on_coordinatePicker_activate(self, widget, *args):
394 if self.cooPickerCheckB.get_active():
395 self.cid = self.canvas.mpl_connect('button_press_event',
396 get_coordinates_cb)
397 self.writeStatusBar('Click on the plot to log coordinates on '+\
398 'the terminal.')
399 elif self.cid != None:
400 self.canvas.mpl_disconnect(self.cid)
401 self.writeStatusBar('Coordinate picker disabled.')
402 else:
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)
410 else:
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=':')
415 self.canvas.draw()
416 def on_info_activate(self, widget, *args):
417 AboutDialog()
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
425 of a function.
427 if not event.inaxes: return
428 print "%3.4f\t%3.4f" % (event.xdata, event.ydata)
431 # Dialogs classes
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()
440 self.load_widgets()
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)
469 # Populate the list
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])
490 # GUI Callbacks
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()
506 print text
508 def on_lineColor_changed(self, *args):
509 text = self.lineColorComboB.get_active_text()
510 print text
512 def on_markerType_changed(self, *args):
513 text = self.markerTypeComboB.get_active_text()
514 print text
516 def on_markerSize_value_changed(self, *args):
517 text = self.markerSizeSpinButt.get_value()
518 print text
520 def on_markerEdgeColor_changed(self, *args):
521 text = self.markerEdgeColorComboB.get_active_text()
522 print text
524 def on_markerFaceColor_changed(self, *args):
525 text = self.markerFaceColorComboB.get_active_text()
526 print 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,
537 callerApp)
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):
551 print "OK Clicked"
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,
569 callerApp)
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
615 else:
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):
622 print "OK"
623 self.get_values()
624 self.set_figsize()
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,
637 callerApp)
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".
648 def __init__(self):
649 GenericSecondaryWindow.__init__(self, 'AboutDialog', gladefile)
650 self.window.set_version(read_version())
653 # Functions
655 def read_version():
656 """ Extract version from README file. """
657 file = open(rootdir+'README')
658 ver = None
659 for line in file:
660 if line.strip().startswith('*Latest Version*'):
661 s = line.split()
662 ver = s[2]
663 break
664 file.close()
665 return ver
667 def plot_from_file(fname):
668 try:
669 datafile = open(fname)
670 except:
671 pass
673 def test():
674 x = arange(100)
675 y = sin(x/10.0)
676 p = PlotFileApp(x, y, title='Random Sequence', debug=True)
677 p.start()
679 def main():
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')
684 p.plot()
685 p.start()
687 if __name__ == '__main__': main()