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

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

Recursion error - move to linux box - see if matplotlib version change helps.

File size: 18.5 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 = 3
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 = 20
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
149################################################################################
150# An object that behaves like a dictionary but is pickled/unpickled from a file.
151################################################################################
152
153class Config(object):
154    """
155    An object that behaves like a dictionary but is persistent:
156
157        cfg = Config('filename')
158        cfg['save'] = 'A saved value'
159        cfg[123] = 'value 123'
160        print cfg['save']
161
162    The values stored in the file 'filename' will be available to any
163    application using that config file.
164    """
165
166    def __init__(self, configfile=None):
167        """
168        __init__(self, String filename=None) -> Config
169        """
170
171        self.delconf = False
172        self.cfgdict = {}
173        self.changed = False
174        if not configfile:
175            self.delconf = True
176            configfile =tempfile.mktemp()
177        self.configfile = os.path.abspath(configfile)
178        if os.path.exists(self.configfile):
179            try:
180                f = open(configfile, "r")
181                u = pickle.Unpickler(f)
182                self.cfgdict = u.load()
183            except pickle.UnpicklingError, e:
184                print e
185            except:
186                pass
187            else:
188                f.close()
189
190    def __setitem__(self, key, value):
191        """
192        Override to allow: cfg[<key>] = <value>
193        """
194        self.cfgdict[key] = value
195        self.changed = True
196
197    def __getitem__(self, key):
198        """
199        Override to allow: <var> = cfg[<key>]
200        """
201        return self.cfgdict.get(key, None)
202
203    def __str__(self):
204        """
205        __str__(self) -> String
206        """
207        return "<config object at %s>" % hex(id(self))
208
209    def getfilename(self):
210        """
211        getfilename(self) -> String filename
212        """
213        return self.configfile
214
215    def setdeleted(self):
216        """
217        setdeleted(self)
218        """
219        self.delconf = True
220
221    def save(self):
222        """
223        save(self)
224        """
225        try:
226            f = open(self.configfile, "w")
227            p = pickle.Pickler(f)
228            p.dump(self.cfgdict)
229        except pickle.PicklingError, e:
230            print e
231        else:
232            f.close()
233        self.changed = False
234
235    def close(self):
236        """
237        close(self)
238        """
239        if self.changed:
240            self.save()
241        if self.delconf:
242            if os.path.exists(self.configfile):
243                os.remove(self.configfile)
244
245    def __del__(self):
246        """
247        __del__(self)
248        """
249        self.close()
250
251################################################################################
252# Plot routines
253################################################################################
254
255##
256# @brief Plot a sequence of data.
257# @param x_data The X data sequence to plot.
258# @param y_data The Y data sequence to plot.
259# @param options A dictionary of plot options.
260def plot_data(x_data, y_data, options):
261    print 'x_data=%s' % str(x_data)
262    print 'y_data=%s' % str(y_data)
263    pylab.plot(x_data, y_data)
264
265    # if user requested a particular Y range
266    if not options[Y_RANGE] is None:
267        try:
268            (minimum, maximum) = options[Y_RANGE].split(',')
269            minimum = float(minimum)
270            maximum = float(maximum)
271        except:
272            error('Sorry, got a bad value for Y range: %s' % options[Y_RANGE])
273        pylab.ylim(ymin=minimum, ymax=maximum)
274
275
276##
277# @brief Get CSV data from a file.
278# @param filename Path to the data file to plot.
279# @param x_hdr The X axis title string (or index).
280# @param y_hdr The Y axis title string (or index).
281# @param options A dictionary of options.
282def getCSVData(filename, x_hdr, y_hdr, options):
283    # get contents of data file
284    # after this, 'header' is list of column header strings
285    #             'data' is a list of lists of data
286    fd = open(filename)
287    c = csv.reader(fd)
288    data = []
289    for row in c:
290        data.append(row)
291    fd.close()
292    header = data[0]
293    del data[0]         # get rid of header in dataset
294
295    # convert column specifiers to 'int' if required
296    try:
297        index = int(x_hdr)
298    except:
299        try:
300            index = header.index(x_hdr)
301        except ValueError:
302            error("Sorry, X column header '%s' isn't in the data file." % x_hdr)
303    options[X_DATACOL] = index
304
305    try:
306        index = int(y_hdr)
307    except:
308        try:
309            index = header.index(y_hdr)
310        except ValueError:
311            error("Sorry, Y column header '%s' isn't in the data file." % y_hdr)
312    options[Y_DATACOL] = index
313   
314    # extract required columns from the data (int at this point)
315    x_col = options[X_DATACOL]
316    y_col = options[Y_DATACOL]
317
318    # get max column number, check requested columns
319    max_col = len(header)
320    if x_col >= max_col or y_col >= max_col:
321        error('Sorry, maximum column number for that file is %d.' % (max_col-1))
322
323    x_label = header[x_col].title()
324    x_data = map(lambda x: x[x_col], data)
325    if x_label == 'Time':
326        x_label = 'Time (hours)'
327        x_data = map(lambda x: float(x)/3600., x_data)
328    y_data = map(lambda x: x[y_col], data)
329    y_label = header[y_col].title()
330
331    options[X_LABEL] = x_label
332    options[Y_LABEL] = y_label
333
334    return (x_data, y_data)
335
336
337##
338# @brief Plot data files.
339# @param filenames List of full pathnames to plot.
340# @param options A dictionary of options.
341def plot_files(filenames, options=None):
342    # set options defaults
343    opts = options
344    if opts is None:
345        opts = {}
346
347##    pylab.title(options.get(TITLE, ''))
348##    pylab.xlabel(options.get(X_LABEL, ''))
349##    pylab.ylabel(options.get(Y_LABEL, ''))
350##    pylab.grid(True)
351##
352    for f in filenames:
353        (x_data, y_data) = getCSVData(f, opts[X_LABEL], opts[Y_LABEL], options)
354        print 'x_data=%s' % str(x_data)
355        print 'y_data=%s' % str(y_data)
356        pylab.plot(x_data, y_data)
357
358    pylab.show()
359
360
361class MyFrame(wx.Frame):
362    def __init__(self, parent, id, title, pos=wx.DefaultPosition,
363                    size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE):
364        # Make the frame
365        wx.Frame.__init__(self, parent, id, title, pos=(50, 50),
366                            size=(FORM_WIDTH, FORM_HEIGHT),
367                            style=(wx.DEFAULT_FRAME_STYLE &
368                                   ~ (wx.RESIZE_BOX | wx.MAXIMIZE_BOX |
369                                      wx.RESIZE_BORDER)))
370
371        p = self.panel = wx.Panel(self, -1)
372
373        if Imported_PyEmbeddedImage:
374            tsunami = getIcon()
375            icon = tsunami.GetIcon()
376            self.SetIcon(icon)
377       
378        self.Center()
379        self.Show(True)
380
381        # start laying out controls
382        Y_OFFSET = START_YOFFSET
383        wx.StaticBox(p, -1, 'CSV files', (3, Y_OFFSET),
384                     size=(BOX_CSV_WIDTH, BOX_CSV_HEIGHT))
385        Y_OFFSET += GEN_DELTAY
386        self.txtCSVFiles = wx.ListBox(p, -1, pos=(8, Y_OFFSET),
387                                      size=(TXT_CSVFILE_WIDTH,
388                                            TXT_CSVFILE_HEIGHT))
389        self.CSVFiles = []
390        Y_OFFSET += TXT_CSVFILE_HEIGHT + MARGIN
391        x = FORM_WIDTH/2 - BUTTON_WIDTH - DOUBLE_BUTTON_OFFSET
392        self.btnAddCSVFile = wx.Button(p, label="Add", pos=(x, Y_OFFSET),
393                                     size=(100, BUTTON_HEIGHT))
394        x = FORM_WIDTH/2 + DOUBLE_BUTTON_OFFSET
395        self.btnDelCSVFile = wx.Button(p, label="Delete", pos=(x, Y_OFFSET),
396                                     size=(100, BUTTON_HEIGHT))
397        Y_OFFSET += BUTTON_HEIGHT + MARGIN
398
399        wx.StaticBox(p, -1, 'Plot details', (3, Y_OFFSET),
400                     size=(BOX_PLOT_WIDTH, BOX_PLOT_HEIGHT))
401        Y_OFFSET += GEN_DELTAY
402        wx.StaticText(p, -1, 'X-Column',
403                      pos=(COLLAB_X_OFFSET, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT)
404        self.cbXColHdr = wx.Choice(p, -1, pos=(COLLAB_X_OFFSET+50, Y_OFFSET),
405                                   size=(80, -1))
406        self.XColHdr = []
407        wx.StaticText(p, -1, 'Y-Column',
408                      pos=(FORM_WIDTH/2+COLLAB_X_OFFSET,
409                           Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT)
410        x = FORM_WIDTH/2 + COLLAB_X_OFFSET
411        self.cbYColHdr = wx.Choice(p, -1, pos=(x+50, Y_OFFSET), size=(80, -1))
412        self.YColHdr = []
413
414        Y_OFFSET += BUTTON_HEIGHT + MARGIN
415        x = FORM_WIDTH/2 - BUTTON_WIDTH/2
416        self.btnPlot = wx.Button(p, label="Plot", pos=(x, Y_OFFSET),
417                                     size=(100, BUTTON_HEIGHT))
418
419        self.Bind(wx.EVT_BUTTON, self.AddCSVFile, self.btnAddCSVFile)
420        self.Bind(wx.EVT_BUTTON, self.DelCSVFile, self.btnDelCSVFile)
421        self.Bind(wx.EVT_BUTTON, self.PlotFiles, self.btnPlot)
422        self.Bind(wx.EVT_CLOSE, self.doStateSave)
423        self.Bind(wx.EVT_CLOSE, self.doStateSave)
424        self.cfg = Config(ConfigFilename)
425        self.common_headers = None
426        self.doStateRestore()
427        self.updateHdrChoices()
428       
429    def AddCSVFile(self, event):
430        """Add a CSV file to the listbox"""
431
432        dlg = wx.FileDialog(self, message='Choose a CSV file',
433                            defaultDir=self.file_dir, 
434                            defaultFile='',
435                            wildcard=CSVWildcard,
436                            style=wx.OPEN | wx.CHANGE_DIR)
437
438        if dlg.ShowModal() == wx.ID_OK:
439            # This returns a list of  files that were selected.
440            path = dlg.GetPath()
441            self.file_dir = dlg.GetDirectory()
442            dlg.Destroy()
443            if path not in self.CSVFiles:
444                headers = self.getHeaders(path)
445                if headers:
446                    common_headers = self.orHeaders(headers)
447                    if not common_headers or len(common_headers) < 2:
448                        self.error("Sorry, file '%s' doesn't have enough headers in common with current files" % path)
449                    else:
450                        self.headers.append(headers)
451                        self.common_headers = common_headers
452                        self.CSVFiles.append(path)
453                        self.txtCSVFiles.Append(path)
454                else:
455                    self.error("Sorry, file '%s' doesn't appear to be a CSV file" % path)
456                self.updateHdrChoices()
457
458    def DelCSVFile(self, event):
459        """Add a CSV file to the listbox"""
460
461        sel = self.txtCSVFiles.GetSelections()
462        if sel:
463            sel = sel[0]
464            self.txtCSVFiles.Delete(sel)
465            del self.CSVFiles[sel]
466            del self.headers[sel]
467            self.common_headers = self.GenCommonHeaders(self.headers)
468            self.updateHdrChoices()
469
470    def doStateSave(self, event):
471        """Save state from 'self' variables."""
472
473        self.cfg['CSVFiles'] = self.CSVFiles
474        self.cfg['XColHdr'] = self.XColHdr
475        self.cfg['XColHdrSelection'] = self.cbXColHdr.GetSelection()
476        self.cfg['YColHdr'] = self.YColHdr
477        self.cfg['YColHdrSelection'] = self.cbYColHdr.GetSelection()
478        self.cfg['file_dir'] = self.file_dir
479
480        self.cfg.save()
481        event.Skip(True)
482
483    def doStateRestore(self):
484        """Restore state to 'self' variables."""
485
486        self.CSVFiles = self.cfg['CSVFiles']
487        if self.CSVFiles is None:
488            self.CSVFiles = []
489        self.XColHdr = self.cfg['XColHdr']
490        if self.XColHdr is None:
491            self.XColHdr = []
492        # -1 here means 'no selection'
493        self.XColHdrSelection = self.cfg['XColHdrSelection', -1]
494        self.YColHdr = self.cfg['YColHdr']
495        if self.YColHdr is None:
496            self.YColHdr = []
497        # -1 here means 'no selection'
498        self.YColHdrSelection = self.cfg['YColHdrSelection', -1]
499        self.file_dir = self.cfg['file_dir']
500        if self.file_dir is None:
501            self.file_dir = os.getcwd()
502
503        # put data into controls
504        self.headers = []
505        for (i, f) in enumerate(self.CSVFiles):
506            headers = self.getHeaders(f)
507            self.headers.append(headers)
508            self.txtCSVFiles.Insert(f, i)
509
510    def getHeaders(self, filename):
511        '''Get header list from a file'''
512
513        try:
514            fd = open(filename, 'r')
515            hdr = fd.readline()
516            fd.close()
517        except:
518            return None
519
520        hdr = hdr.split(',')
521        hdr = [x.strip() for x in hdr]
522
523        return hdr
524       
525    def updateHdrChoices(self):
526        '''Update choice controls with header lists.
527
528        Keep initially visible strings visible afterwards, if posssible.
529        '''
530
531        selected_x = self.cbXColHdr.GetStringSelection()
532        selected_y = self.cbYColHdr.GetStringSelection()
533       
534        self.cbXColHdr.Clear()
535        self.cbYColHdr.Clear()
536
537        if self.common_headers:
538            self.cbXColHdr.Enable()
539            self.cbYColHdr.Enable()
540            self.btnPlot.Enable()
541            for h in self.common_headers:
542                self.cbXColHdr.Append(h)
543                self.cbYColHdr.Append(h)
544            if selected_x in self.common_headers:
545                index = self.common_headers.index(selected_x)
546                self.cbXColHdr.SetSelection(index)
547            if selected_y in self.common_headers:
548                index = self.common_headers.index(selected_y)
549                self.cbYColHdr.SetSelection(index)
550        else:
551            self.cbXColHdr.Disable()
552            self.cbYColHdr.Disable()
553            self.btnPlot.Disable()
554
555    def orHeaders(self, new_header):
556        '''Update X & Y column header choices'''
557
558        if self.common_headers:
559            result = [x for x in new_header if x in self.common_headers]
560        else:
561            result = new_header
562        return result
563
564    def GenCommonHeaders(self, headers):
565        '''Get new set of common headers'''
566
567        result = []
568        for header in headers:
569            if result:
570                result = [x for x in result if x in header]
571            else:
572                result = header
573        return result
574
575    def PlotFiles(self, event):
576        selected_x = self.cbXColHdr.GetStringSelection()
577        selected_y = self.cbYColHdr.GetStringSelection()
578
579        if selected_x and selected_y:
580            options = {}
581            # set X and Y axis labels
582            options[X_LABEL] = selected_x
583            options[Y_LABEL] = selected_y
584            options[SIZE] = '800,600'
585            options[TITLE] = 'TITLE'
586
587            plot_files(self.CSVFiles, options) 
588           
589    def error(self, msg):
590        dlg = wx.MessageDialog(self, msg, 'Error',
591                               wx.OK | wx.ICON_INFORMATION)
592        dlg.ShowModal()
593        dlg.Destroy()
594
595       
596################################################################################
597# Code to do the GUI
598################################################################################
599
600if __name__ == '__main__':
601    app = wx.App()
602    frame = MyFrame(None, -1, '%s %s' % (APP_NAME, APP_VERSION),
603                    size=(FORM_WIDTH, FORM_HEIGHT))
604    app.SetTopWindow(frame)
605    app.MainLoop()
606
Note: See TracBrowser for help on using the repository browser.