source: misc/tools/plotcsv/plotcsv.py @ 6351

Last change on this file since 6351 was 6351, checked in by rwilson, 15 years ago

Cleanup and attempt to get matplotlib 0.98.5.2 working.

File size: 17.9 KB
Line 
1#!/usr/bin/env python
2
3'''A module to draw a graph on the screen given a data file.
4
5The code is written such that it may be run as a program or imported
6and used as a module.
7'''
8
9import sys
10import os.path
11import types
12import csv
13import time
14import getopt
15import pylab
16try:
17    import cpickle
18except:
19    import pickle
20
21try:
22    import wx
23except ImportError:
24    import tkinter_error
25    msg = ('Sorry, you have to install WxPython.\n'
26           'Get it from either:\n'
27           r'   N:\georisk\downloads\event_selection, or' '\n'
28           '   [http://www.wxpython.org/download.php#binaries].')
29    tkinter_error.tkinter_error(msg)
30    print msg
31    sys.exit(1)
32
33Imported_PyEmbeddedImage = True
34try:
35    from wx.lib.embeddedimage import PyEmbeddedImage
36except ImportError:
37    Imported_PyEmbeddedImage = False
38
39
40# program name and version
41APP_NAME = 'plotcsv'
42APP_VERSION = '0.1'
43
44# name of the configuration filename
45# GUI values are saved in this file for next time
46ConfigFilename = '%s.cfg' % APP_NAME
47
48# GUI definitions
49BUTTON_WIDTH = 100
50BUTTON_HEIGHT = 30
51
52MARGIN = 10
53
54LAB_CTRL_OFFSET = 5
55
56BOX_WIDTH = 400
57BOX_HEIGHT = 360
58BOX_CSV_WIDTH = 400
59BOX_CSV_HEIGHT = 265
60BOX_PLOT_WIDTH = 400
61BOX_PLOT_HEIGHT = 90
62
63TXT_CSVFILE_WIDTH = 390
64TXT_CSVFILE_HEIGHT = 200
65
66COLLAB_X_OFFSET = 25
67
68CHBOX_HEIGHT = 30
69CHBOX_WIDTH = 100
70
71FORM_WIDTH = BOX_WIDTH + 15
72FORM_HEIGHT = 405
73
74START_YOFFSET = 7
75OUT_TEXT_XOFFSET = 8
76OUT_TEXT_YOFFSET = 11
77OUTPUT_BOX_HEIGHT = 45
78
79GEN_YOFFSET = 15
80GEN_DELTAY = 15
81GEN_LABELXOFFSET = 5
82GEN_TEXTXOFFSET = BOX_WIDTH/2 + GEN_LABELXOFFSET + 10
83
84TEXTLIST_WIDTH = BOX_WIDTH - 10
85TEXTLIST_HEIGHT = 200
86
87CTL_WIDTH = BOX_WIDTH / 2
88CTL_HEIGHT = 22
89
90DOUBLE_BUTTON_OFFSET = 8
91
92COMBO_FUDGE = 2
93
94# Copies of various parameters
95Region = ''
96GaugeNumber = ''
97MinimumHeight = ''
98MaximumHeight = ''
99
100# Flag strings - keys in the 'options' dictionary
101X_DATACOL = 'x_datacol'
102Y_DATACOL = 'y_datacol'
103X_RANGE = 'x_range'
104Y_RANGE = 'y_range'
105FILENAME = 'filename'
106SIZE = 'size'
107TITLE = 'title'
108X_LABEL = 'x_label'
109Y_LABEL = 'y_label'
110
111CSVWildcard = 'CSV files (*.csv)|*.csv|All files (*.*)|*.*'
112
113# Flag strings - keys in the 'options' dictionary
114X_DATACOL = 'x_datacol'
115Y_DATACOL = 'y_datacol'
116X_RANGE = 'x_range'
117Y_RANGE = 'y_range'
118FILENAME = 'filename'
119SIZE = 'size'
120TITLE = 'title'
121X_LABEL = 'x_label'
122Y_LABEL = 'y_label'
123
124
125################################################################################
126# This code was generated by img2py.py
127# Embed the ICON image here, so we don't need an external *.ico file.
128################################################################################
129
130if Imported_PyEmbeddedImage:
131    def getIcon():
132        return PyEmbeddedImage(
133    "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAApxJ"
134    "REFUWIXtlr9O40AQh2edWwsKJFAarGtcRaElJWkoo4TGZegocqLME6TOC6CtaWjCFXRUfgJE"
135    "xStsiivuKiQ4ND8Kx/bu+k+cUKRhpC0ie+f7sjs7a0FEoB3GDyIiYDcOQgjydkI24lvgW2Bj"
136    "gctLolaLaG+P6OiI6PCQSIh89HpEy+VmOdE0lAKI1o8oArSuycMKAQegVQ9qDJeymUCdhGIF"
137    "yRLE1FzAhZ+dATc3wN0d8PiYg9z3XAkLbgl0u4DnAePxWvja5XXen0zK4RFHhoA5lNoabs4z"
138    "Uw4finDN2hAIw/xtKQGltoanMZut5k4U6K0IB2AIaJ0QVrQPT+LaU1vD0xg+2PDuSwJXCggC"
139    "UwAoSLyTxC9SpfAFLxBwAJ99THlavg1uwS0i0LHGcGiurHMKbucav0Uu8V9I/JsrK7FmjRM+"
140    "yRL77CPmuBbefUngxeNqCKR7HpDGPUWFmkjhEUf5v1qNkMNMoqzaNWtMJsU+kQmMx/bDq4HG"
141    "68CW0LdzC97iFtrctiSmPC2Fpzvc7yenvd9PfmcC+/s56/x8tedGTehjQnQvssSSJRQrxBwj"
142    "5LCwIi68KqwV8LykHz0/mxuuoa8GiBZ5YvnhQXFeF2USTeCWQFVo1oheBzn8jaCuPatZpRId"
143    "7sBnHyMeJXCtgaen2vNbK+AWnHwXUJNiYZZP1slylt1Sp6eZVKVAAc4S6u/c6hO1Em7JV1yV"
144    "pQKl8HTPnWZVKpH14PX3dUGgFm4ub5WEewvNZvZc54KxBBrB6ySmU+DgoHgPu2FIZAIbwask"
145    "zNHrrf9oCIxPMrO3N4LXSYQhEMeNpmcCKVywaA43JUYjwPeBTqcx3BIIOYTHHi74YjP4F4OI"
146    "IIgI9JOI2kS0JKI/m33TfzU+ASG0bvT61fpzAAAAAElFTkSuQmCC")
147
148
149def error(msg):
150    print msg
151    sys.exit(10)
152
153
154################################################################################
155# An object that behaves like a dictionary but is pickled/unpickled from a file.
156################################################################################
157
158class Config(object):
159    """An object that behaves like a dictionary but is persistent:
160
161        cfg = Config('filename')
162        cfg['save'] = 'A saved value'
163        cfg[123] = 'value 123'
164        print cfg['save']
165
166    The values stored in the file 'filename' will be available to any
167    application using that config file.
168    """
169
170    def __init__(self, configfile=None):
171        """__init__(self, String filename=None) -> Config"""
172
173        self.delconf = False
174        self.cfgdict = {}
175        self.changed = False
176        if not configfile:
177            self.delconf = True
178            configfile =tempfile.mktemp()
179        self.configfile = os.path.abspath(configfile)
180        if os.path.exists(self.configfile):
181            try:
182                f = open(configfile, "r")
183                u = pickle.Unpickler(f)
184                self.cfgdict = u.load()
185            except pickle.UnpicklingError, e:
186                print e
187            except:
188                pass
189            else:
190                f.close()
191
192    def __setitem__(self, key, value):
193        """Override to allow: cfg[<key>] = <value>"""
194       
195        self.cfgdict[key] = value
196        self.changed = True
197
198    def __getitem__(self, key):
199        """Override to allow: <var> = cfg[<key>]"""
200       
201        return self.cfgdict.get(key, None)
202
203    def __str__(self):
204        """__str__(self) -> String"""
205       
206        return "<config object at %s>" % hex(id(self))
207
208    def getfilename(self):
209        """getfilename(self) -> String filename"""
210       
211        return self.configfile
212
213    def setdeleted(self):
214        """setdeleted(self)"""
215       
216        self.delconf = True
217
218    def save(self):
219        """save(self)"""
220       
221        try:
222            f = open(self.configfile, "w")
223            p = pickle.Pickler(f)
224            p.dump(self.cfgdict)
225        except pickle.PicklingError, e:
226            print e
227        else:
228            f.close()
229        self.changed = False
230
231    def close(self):
232        """close(self)"""
233       
234        if self.changed:
235            self.save()
236        if self.delconf:
237            if os.path.exists(self.configfile):
238                os.remove(self.configfile)
239
240    def __del__(self):
241        """__del__(self)"""
242       
243        self.close()
244       
245
246################################################################################
247# Plot routines
248################################################################################
249
250##
251# @brief Get CSV data from a file.
252# @param filename Path to the data file to plot.
253# @param x_hdr The X axis title string.
254# @param y_hdr The Y axis title string.
255def getCSVData(filename, x_hdr, y_hdr):
256    # get contents of data file
257    # after this, 'header' is list of column header strings
258    #             'data' is a list of lists of data
259    fd = open(filename)
260    c = csv.reader(fd)
261    data = []
262    for row in c:
263        data.append(row)
264    fd.close()
265    header = data[0]
266    del data[0]         # get rid of header in dataset
267
268    # get int index values for column headers
269    try:
270        x_index = header.index(x_hdr)
271    except ValueError:
272        error("Sorry, X column header '%s' isn't in the data file." % x_hdr)
273
274    try:
275        y_index = header.index(y_hdr)
276    except ValueError:
277        error("Sorry, Y column header '%s' isn't in the data file." % y_hdr)
278
279    # get appropriate columns from data[]
280    x_data = map(lambda x: x[x_index], data)
281    if x_hdr == 'time':
282        x_data = map(lambda x: float(x)/3600., x_data)
283    y_data = map(lambda x: x[y_index], data)
284
285    return (x_data, y_data)
286
287
288##
289# @brief Plot data files.
290# @param filenames List of full pathnames to plot.
291# @param x_hdr The X axis label string.
292# @param y_hdr The Y axis label string.
293def plot_files(filenames, x_hdr, y_hdr):
294    pylab.rc('axes', linewidth=2)
295    pylab.xlabel(x_hdr.title())
296    pylab.ylabel(y_hdr.title())
297    pylab.grid(True)
298
299    for f in filenames:
300        (x_data, y_data) = getCSVData(f, x_hdr, y_hdr)
301        pylab.plot(x_data, y_data)
302
303    (min_y, max_y) = pylab.ylim()
304    range_y = max_y - min_y
305    add_y = float(range_y) / 5
306    pylab.ylim((min_y, max_y+add_y))
307    pylab.legend(filenames, 'upper left')
308
309    pylab.show()
310#    pylab.close()
311
312
313################################################################################
314# GUI routines
315################################################################################
316
317class MyFrame(wx.Frame):
318    def __init__(self, parent, id, title, pos=wx.DefaultPosition,
319                    size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE):
320        '''Lay out the GUI form'''
321       
322        # Make the frame
323        wx.Frame.__init__(self, parent, id, title, pos=(50, 50),
324                            size=(FORM_WIDTH, FORM_HEIGHT),
325                            style=(wx.DEFAULT_FRAME_STYLE &
326                                   ~ (wx.RESIZE_BOX | wx.MAXIMIZE_BOX |
327                                      wx.RESIZE_BORDER)))
328
329        p = self.panel = wx.Panel(self, -1)
330
331        if Imported_PyEmbeddedImage:
332            tsunami = getIcon()
333            icon = tsunami.GetIcon()
334            self.SetIcon(icon)
335       
336        self.Center()
337        self.Show(True)
338
339        # start laying out controls
340        Y_OFFSET = START_YOFFSET
341        wx.StaticBox(p, -1, 'CSV files', (3, Y_OFFSET),
342                     size=(BOX_CSV_WIDTH, BOX_CSV_HEIGHT))
343        Y_OFFSET += GEN_DELTAY
344        self.txtCSVFiles = wx.ListBox(p, -1, pos=(8, Y_OFFSET),
345                                      size=(TXT_CSVFILE_WIDTH,
346                                            TXT_CSVFILE_HEIGHT))
347        self.CSVFiles = []
348        Y_OFFSET += TXT_CSVFILE_HEIGHT + MARGIN
349        x = FORM_WIDTH/2 - BUTTON_WIDTH - DOUBLE_BUTTON_OFFSET
350        self.btnAddCSVFile = wx.Button(p, label="Add", pos=(x, Y_OFFSET),
351                                     size=(100, BUTTON_HEIGHT))
352        x = FORM_WIDTH/2 + DOUBLE_BUTTON_OFFSET
353        self.btnDelCSVFile = wx.Button(p, label="Delete", pos=(x, Y_OFFSET),
354                                     size=(100, BUTTON_HEIGHT))
355        Y_OFFSET += BUTTON_HEIGHT + MARGIN
356
357        wx.StaticBox(p, -1, 'Plot files', (3, Y_OFFSET),
358                     size=(BOX_PLOT_WIDTH, BOX_PLOT_HEIGHT))
359        Y_OFFSET += GEN_DELTAY
360        wx.StaticText(p, -1, 'X-Column',
361                      pos=(COLLAB_X_OFFSET, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT)
362        self.cbXColHdr = wx.Choice(p, -1, pos=(COLLAB_X_OFFSET+65, Y_OFFSET),
363                                   size=(80, -1))
364        self.XColHdr = []
365        wx.StaticText(p, -1, 'Y-Column',
366                      pos=(FORM_WIDTH/2+COLLAB_X_OFFSET,
367                           Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT)
368        x = FORM_WIDTH/2 + COLLAB_X_OFFSET
369        self.cbYColHdr = wx.Choice(p, -1, pos=(x+65, Y_OFFSET), size=(80, -1))
370        self.YColHdr = []
371
372        Y_OFFSET += BUTTON_HEIGHT + MARGIN
373        x = FORM_WIDTH/2 - BUTTON_WIDTH/2
374        self.btnPlot = wx.Button(p, label="Plot", pos=(x, Y_OFFSET),
375                                     size=(100, BUTTON_HEIGHT))
376
377        # bind controls/events to handlers
378        self.Bind(wx.EVT_BUTTON, self.AddCSVFile, self.btnAddCSVFile)
379        self.Bind(wx.EVT_BUTTON, self.DelCSVFile, self.btnDelCSVFile)
380        self.Bind(wx.EVT_BUTTON, self.PlotFiles, self.btnPlot)
381       
382        self.Bind(wx.EVT_CLOSE, self.doStateSave)
383
384        # restore saved values
385        self.common_headers = None
386        self.cfg = Config(ConfigFilename)
387        self.doStateRestore()
388        self.updateHdrChoices()
389       
390    def AddCSVFile(self, event):
391        """Add a CSV file to the listbox"""
392
393        dlg = wx.FileDialog(self, message='Choose a CSV file',
394                            defaultDir=self.file_dir, 
395                            defaultFile='',
396                            wildcard=CSVWildcard,
397                            style=wx.OPEN | wx.CHANGE_DIR)
398
399        if dlg.ShowModal() == wx.ID_OK:
400            # This returns a list of  files that were selected.
401            path = dlg.GetPath()
402            self.file_dir = dlg.GetDirectory()
403            dlg.Destroy()
404            if path not in self.CSVFiles:
405                headers = self.getHeaders(path)
406                if headers:
407                    common_headers = self.orHeaders(headers)
408                    if not common_headers or len(common_headers) < 2:
409                        self.error("Sorry, file '%s' doesn't have enough headers in common with current files" % path)
410                    else:
411                        self.headers.append(headers)
412                        self.common_headers = common_headers
413                        self.CSVFiles.append(path)
414                        self.txtCSVFiles.Append(path)
415                else:
416                    self.error("Sorry, file '%s' doesn't appear to be a CSV file" % path)
417                self.updateHdrChoices()
418
419    def DelCSVFile(self, event):
420        """Delete a CSV file from the listbox"""
421
422        sel = self.txtCSVFiles.GetSelections()
423        if sel:
424            sel = sel[0]
425            self.txtCSVFiles.Delete(sel)
426            del self.CSVFiles[sel]
427            del self.headers[sel]
428            self.common_headers = self.GenCommonHeaders(self.headers)
429            self.updateHdrChoices()
430
431    def doStateSave(self, event):
432        """Save state from 'self' variables & controls."""
433
434        self.cfg['CSVFiles'] = self.CSVFiles
435        self.cfg['XColHdr'] = self.XColHdr
436        self.cfg['XColHdrSelection'] = self.cbXColHdr.GetSelection()
437        self.cfg['YColHdr'] = self.YColHdr
438        self.cfg['YColHdrSelection'] = self.cbYColHdr.GetSelection()
439        self.cfg['file_dir'] = self.file_dir
440
441        self.cfg.save()
442        event.Skip(True)
443
444    def doStateRestore(self):
445        """Restore state to 'self' variables."""
446
447        self.CSVFiles = self.cfg['CSVFiles']
448        if self.CSVFiles is None:
449            self.CSVFiles = []
450        self.XColHdr = self.cfg['XColHdr']
451        if self.XColHdr is None:
452            self.XColHdr = []
453        # -1 here means 'no selection'
454        self.XColHdrSelection = self.cfg['XColHdrSelection']
455        self.YColHdr = self.cfg['YColHdr']
456        if self.YColHdr is None:
457            self.YColHdr = []
458        # -1 here means 'no selection'
459        self.YColHdrSelection = self.cfg['YColHdrSelection']
460        self.file_dir = self.cfg['file_dir']
461        if self.file_dir is None:
462            self.file_dir = os.getcwd()
463
464        # put data into controls
465        self.headers = []
466        for (i, f) in enumerate(self.CSVFiles):
467            headers = self.getHeaders(f)
468            self.headers.append(headers)
469            self.txtCSVFiles.Insert(f, i)
470        self.common_headers = self.GenCommonHeaders(self.headers)
471        self.updateHdrChoices()
472
473        if self.XColHdrSelection:
474            self.cbXColHdr.SetSelection(self.XColHdrSelection)
475        if self.YColHdrSelection:
476            self.cbYColHdr.SetSelection(self.YColHdrSelection)
477
478    def getHeaders(self, filename):
479        '''Get header list from a file'''
480
481        try:
482            fd = open(filename, 'r')
483            hdr = fd.readline()
484            fd.close()
485        except:
486            return None
487
488        hdr = hdr.split(',')
489        hdr = [x.strip() for x in hdr]
490
491        return hdr
492       
493    def updateHdrChoices(self):
494        '''Update choice controls with header lists.
495
496        Disable controls if no CSV files (ie, no headers).
497        Keep selections visible afterwards, if possible.
498        '''
499
500        # get current selections, if any
501        selected_x = self.cbXColHdr.GetStringSelection()
502        selected_y = self.cbYColHdr.GetStringSelection()
503       
504        self.cbXColHdr.Clear()
505        self.cbYColHdr.Clear()
506
507        if self.common_headers:
508            self.cbXColHdr.Enable()
509            self.cbYColHdr.Enable()
510            self.btnPlot.Enable()
511            for h in self.common_headers:
512                self.cbXColHdr.Append(h)
513                self.cbYColHdr.Append(h)
514            if selected_x in self.common_headers:
515                index = self.common_headers.index(selected_x)
516                self.cbXColHdr.SetSelection(index)
517            if selected_y in self.common_headers:
518                index = self.common_headers.index(selected_y)
519                self.cbYColHdr.SetSelection(index)
520        else:
521            self.cbXColHdr.Disable()
522            self.cbYColHdr.Disable()
523            self.btnPlot.Disable()
524
525    def orHeaders(self, new_header):
526        '''Update X & Y column header choices.
527
528        Return new 'common headers' list from current
529        self.common_headers and the supplied new_header list.
530        '''
531
532        if self.common_headers:
533            result = [x for x in new_header if x in self.common_headers]
534        else:
535            result = new_header
536        return result
537
538    def GenCommonHeaders(self, headers):
539        '''Get new set of common headers.
540
541        Return a new 'common header' list given a
542        list of header lists.
543        '''
544
545        result = []
546        for header in headers:
547            if result:
548                result = [x for x in result if x in header]
549            else:
550                result = header
551        return result
552
553    def PlotFiles(self, event):
554        '''Plot files in the CSV file listbox.'''
555       
556        selected_x = self.cbXColHdr.GetStringSelection()
557        selected_y = self.cbYColHdr.GetStringSelection()
558
559        if selected_x and selected_y:
560            plot_files(self.CSVFiles, selected_x, selected_y) 
561           
562    def error(self, msg):
563        '''Issue xwPython error message.'''
564       
565        dlg = wx.MessageDialog(self, msg, 'Error',
566                               wx.OK | wx.ICON_INFORMATION)
567        dlg.ShowModal()
568        dlg.Destroy()
569
570       
571################################################################################
572# Mainline code - start the application.
573################################################################################
574
575if __name__ == '__main__':
576    app = wx.App()
577    frame = MyFrame(None, -1, '%s %s' % (APP_NAME, APP_VERSION),
578                    size=(FORM_WIDTH, FORM_HEIGHT))
579    app.SetTopWindow(frame)
580    app.MainLoop()
581
Note: See TracBrowser for help on using the repository browser.