#!/usr/bin/env python '''A module to draw a graph on the screen given a data file. The code is written such that it may be run as a program or imported and used as a module. ''' import sys import os.path import types import csv import time import getopt import string try: import cpickle as pickle except: import pickle try: import pylab except ImportError: import tkinter_error msg = ('Sorry, you have to install matplotlib.\n' 'Get it from either:\n' r' N:\georisk\downloads\matplotlib, or' '\n' ' [http://matplotlib.sourceforge.net/].\n\n' 'You will also need to have numpy installed:\n' r' N:\georisk\downloads\numpy') tkinter_error.tkinter_error(msg) print msg sys.exit(1) try: import wx except ImportError: import tkinter_error msg = ('Sorry, you have to install WxPython.\n' 'Get it from either:\n' r' N:\georisk\downloads\event_selection, or' '\n' ' [http://www.wxpython.org/download.php#binaries].') tkinter_error.tkinter_error(msg) print msg sys.exit(1) Imported_PyEmbeddedImage = True try: from wx.lib.embeddedimage import PyEmbeddedImage except ImportError: Imported_PyEmbeddedImage = False # program name and version APP_NAME = 'plotcsv' APP_VERSION = '0.7' # name of the configuration filename # GUI values are saved in this file for next time ConfigFilename = '%s.cfg' % APP_NAME # GUI definitions BUTTON_WIDTH = 100 BUTTON_HEIGHT = 30 MARGIN = 10 LAB_CTRL_OFFSET = 5 BOX_WIDTH = 400 BOX_HEIGHT = 360 BOX_CSV_WIDTH = 400 BOX_CSV_HEIGHT = 265 BOX_PLOT_WIDTH = 400 BOX_PLOT_HEIGHT = 140+25+25+30 TXT_CSVFILE_WIDTH = 390 TXT_CSVFILE_HEIGHT = 200 COLLAB_X_OFFSET = 25 CHBOX_HEIGHT = 30 CHBOX_WIDTH = 100 FORM_WIDTH = BOX_WIDTH + 15 FORM_HEIGHT = 450+25+25+30 START_YOFFSET = 7 OUT_TEXT_XOFFSET = 8 OUT_TEXT_YOFFSET = 11 OUTPUT_BOX_HEIGHT = 45 GEN_YOFFSET = 15 GEN_DELTAY = 15 GEN_LABELXOFFSET = 5 GEN_TEXTXOFFSET = BOX_WIDTH/2 + GEN_LABELXOFFSET + 10 TEXTLIST_WIDTH = BOX_WIDTH - 10 TEXTLIST_HEIGHT = 200 CTL_WIDTH = BOX_WIDTH / 2 CTL_HEIGHT = 22 DOUBLE_BUTTON_OFFSET = 8 COMBO_FUDGE = 2 # Copies of various parameters Region = '' GaugeNumber = '' MinimumHeight = '' MaximumHeight = '' # Flag strings - keys in the 'options' dictionary X_DATACOL = 'x_datacol' Y_DATACOL = 'y_datacol' X_RANGE = 'x_range' Y_RANGE = 'y_range' FILENAME = 'filename' SIZE = 'size' TITLE = 'title' X_LABEL = 'x_label' Y_LABEL = 'y_label' CSVWildcard = 'CSV files (*.csv)|*.csv|All files (*.*)|*.*' # Flag strings - keys in the 'options' dictionary X_DATACOL = 'x_datacol' Y_DATACOL = 'y_datacol' X_RANGE = 'x_range' Y_RANGE = 'y_range' FILENAME = 'filename' SIZE = 'size' TITLE = 'title' X_LABEL = 'x_label' Y_LABEL = 'y_label' ################################################################################ # This code was generated by img2py.py # Embed the ICON image here, so we don't need an external *.ico file. ################################################################################ if Imported_PyEmbeddedImage: def getIcon(): return PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAApxJ" "REFUWIXtlr9O40AQh2edWwsKJFAarGtcRaElJWkoo4TGZegocqLME6TOC6CtaWjCFXRUfgJE" "xStsiivuKiQ4ND8Kx/bu+k+cUKRhpC0ie+f7sjs7a0FEoB3GDyIiYDcOQgjydkI24lvgW2Bj" "gctLolaLaG+P6OiI6PCQSIh89HpEy+VmOdE0lAKI1o8oArSuycMKAQegVQ9qDJeymUCdhGIF" "yRLE1FzAhZ+dATc3wN0d8PiYg9z3XAkLbgl0u4DnAePxWvja5XXen0zK4RFHhoA5lNoabs4z" "Uw4finDN2hAIw/xtKQGltoanMZut5k4U6K0IB2AIaJ0QVrQPT+LaU1vD0xg+2PDuSwJXCggC" "UwAoSLyTxC9SpfAFLxBwAJ99THlavg1uwS0i0LHGcGiurHMKbucav0Uu8V9I/JsrK7FmjRM+" "yRL77CPmuBbefUngxeNqCKR7HpDGPUWFmkjhEUf5v1qNkMNMoqzaNWtMJsU+kQmMx/bDq4HG" "68CW0LdzC97iFtrctiSmPC2Fpzvc7yenvd9PfmcC+/s56/x8tedGTehjQnQvssSSJRQrxBwj" "5LCwIi68KqwV8LykHz0/mxuuoa8GiBZ5YvnhQXFeF2USTeCWQFVo1oheBzn8jaCuPatZpRId" "7sBnHyMeJXCtgaen2vNbK+AWnHwXUJNiYZZP1slylt1Sp6eZVKVAAc4S6u/c6hO1Em7JV1yV" "pQKl8HTPnWZVKpH14PX3dUGgFm4ub5WEewvNZvZc54KxBBrB6ySmU+DgoHgPu2FIZAIbwask" "zNHrrf9oCIxPMrO3N4LXSYQhEMeNpmcCKVywaA43JUYjwPeBTqcx3BIIOYTHHi74YjP4F4OI" "IIgI9JOI2kS0JKI/m33TfzU+ASG0bvT61fpzAAAAAElFTkSuQmCC") ################################################################################ # An object that behaves like a dictionary but is pickled/unpickled from a file. # Used to save state in the application. ################################################################################ class Config(object): """An object that behaves like a dictionary but is persistent: cfg = Config('filename') cfg['save'] = 'A saved value' cfg[123] = 'value 123' print cfg['save'] The values stored in the file 'filename' will be available to any application using that config file. """ def __init__(self, configfile=None): """__init__(self, String filename=None) -> Config""" self.delconf = False self.cfgdict = {} self.changed = False if not configfile: self.delconf = True configfile =tempfile.mktemp() self.configfile = os.path.abspath(configfile) if os.path.exists(self.configfile): try: f = open(configfile, "r") u = pickle.Unpickler(f) self.cfgdict = u.load() except pickle.UnpicklingError, e: print e except: pass else: f.close() def __setitem__(self, key, value): """Override to allow: cfg[] = """ self.cfgdict[key] = value self.changed = True def get(self, key, default=None): """Override to allow: = cfg.get(, default=None)""" return self.cfgdict.get(key, default) def __getitem__(self, key): """Override to allow: = cfg[]""" return self.cfgdict.get(key, None) def __str__(self): """__str__(self) -> String""" return "" % hex(id(self)) def getfilename(self): """getfilename(self) -> String filename""" return self.configfile def setdeleted(self): """setdeleted(self)""" self.delconf = True def save(self): """save(self)""" try: f = open(self.configfile, "w") p = pickle.Pickler(f) p.dump(self.cfgdict) except pickle.PicklingError, e: print e else: f.close() self.changed = False def close(self): """close(self)""" if self.changed: self.save() if self.delconf: if os.path.exists(self.configfile): os.remove(self.configfile) def __del__(self): """__del__(self)""" self.close() ################################################################################ # Plot routines ################################################################################ ## # @brief Get CSV data from a file. # @param filename Path to the data file to plot. # @param x_hdr The X axis title string. # @param y_hdr The Y axis title string. def getCSVData(filename, x_hdr, y_hdr): # get contents of data file # after this, 'header' is list of column header strings # 'data' is a list of lists of data fd = open(filename) c = csv.reader(fd) data = [] for row in c: data.append(row) fd.close() header = data[0] del data[0] # get rid of header in dataset # ensure header strings don't have leading/trailing spaces header = map(string.strip, header) # get int index values for column headers try: x_index = header.index(x_hdr) except ValueError: TheFrame.error("Sorry, X column header '%s' isn't in data file '%s'." % (x_hdr, filename)) return None try: y_index = header.index(y_hdr) except ValueError: TheFrame.error("Sorry, Y column header '%s' isn't in data file '%s'." % (y_hdr, filename)) return None # get appropriate columns from data[] x_data = map(lambda x: float(x[x_index]), data) if x_hdr == 'time': x_data = map(lambda x: float(x)/3600., x_data) y_data = map(lambda x: float(x[y_index]), data) return (x_data, y_data) ## # @brief Plot data files. # @param filenames List of full pathnames to plot. # @param x_hdr The X axis data header string. # @param y_hdr The Y axis data header string. # @param title The string used to title the graph. # @param legend True if a legend is to be displayed. # @param legend_path True if legend is to contain full file paths. # @param grid True if grid is to be displayed. # @param fontsize Point size of font. # @param plot_width Width of plot lines. # @param x_label Override X axis label. # @param y_label Override Y axis label. def plot_files(filenames, x_hdr, y_hdr, title='', legend=False, legend_path=False, grid=False, fontsize=10, plot_width=2, x_label=None, y_label=None): if x_label is None: x_label = x_hdr if y_label is None: y_label = y_hdr pylab.rc('axes', linewidth=2) pylab.rc('lines', linewidth=float(plot_width)) pylab.rc(('xtick', 'ytick'), labelsize=float(fontsize)*0.75,) pylab.rc(('xtick.major', 'ytick.major'), size=5, pad=5) pylab.rc('axes', labelsize=float(fontsize)) pylab.xlabel(x_label) pylab.ylabel(y_label) pylab.grid(grid) for f in filenames: result = getCSVData(f, x_hdr, y_hdr) if result is None: # some sort of error pylab.close() return (x_data, y_data) = result pylab.plot(x_data, y_data) if title: pylab.title(title, fontsize=float(fontsize)*1.5) if legend: pylab.rc('legend', fancybox=True) (min_y, max_y) = pylab.ylim() range_y = max_y - min_y add_y = float(range_y) / 5 pylab.ylim((min_y, max_y+add_y)) legend_names = filenames if not legend_path: legend_names = [os.path.basename(x) for x in filenames] pylab.legend(legend_names, 'upper right') pylab.show() pylab.close() ################################################################################ # GUI routines ################################################################################ class MyFrame(wx.Frame): def __init__(self, parent, id, title, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE): '''Lay out the GUI form''' # Make the frame wx.Frame.__init__(self, parent, id, title, pos=(50, 50), size=(FORM_WIDTH, FORM_HEIGHT), style=(wx.DEFAULT_FRAME_STYLE & ~ (wx.RESIZE_BOX | wx.MAXIMIZE_BOX | wx.RESIZE_BORDER))) p = self.panel = wx.Panel(self, -1) if Imported_PyEmbeddedImage: tsunami = getIcon() icon = tsunami.GetIcon() self.SetIcon(icon) self.Center() self.Show(True) # start laying out controls Y_OFFSET = START_YOFFSET wx.StaticBox(p, -1, 'CSV files', (3, Y_OFFSET), size=(BOX_CSV_WIDTH, BOX_CSV_HEIGHT)) Y_OFFSET += GEN_DELTAY self.txtCSVFiles = wx.ListBox(p, -1, pos=(8, Y_OFFSET), size=(TXT_CSVFILE_WIDTH, TXT_CSVFILE_HEIGHT)) self.CSVFiles = [] Y_OFFSET += TXT_CSVFILE_HEIGHT + MARGIN x = FORM_WIDTH/2 - BUTTON_WIDTH - DOUBLE_BUTTON_OFFSET self.btnAddCSVFile = wx.Button(p, label="Add", pos=(x, Y_OFFSET), size=(100, BUTTON_HEIGHT)) x = FORM_WIDTH/2 + DOUBLE_BUTTON_OFFSET self.btnDelCSVFile = wx.Button(p, label="Delete All", pos=(x, Y_OFFSET), size=(100, BUTTON_HEIGHT)) Y_OFFSET += BUTTON_HEIGHT + MARGIN wx.StaticBox(p, -1, 'Plot files', (3, Y_OFFSET), size=(BOX_PLOT_WIDTH, BOX_PLOT_HEIGHT)) Y_OFFSET += GEN_DELTAY wx.StaticText(p, -1, 'X-Column', pos=(COLLAB_X_OFFSET, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) self.cbXColHdr = wx.Choice(p, -1, pos=(COLLAB_X_OFFSET+50, Y_OFFSET), size=(80, -1)) self.XColHdr = [] wx.StaticText(p, -1, 'Y-Column', pos=(FORM_WIDTH/2, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) x = FORM_WIDTH/2+50 self.cbYColHdr = wx.Choice(p, -1, pos=(x, Y_OFFSET), size=(80, -1)) self.YColHdr = [] Y_OFFSET += GEN_DELTAY*1.5 wx.StaticText(p, -1, 'X-Label', pos=(COLLAB_X_OFFSET, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) self.txtXLabel = wx.TextCtrl(p, -1, "", size=(100, -1), pos=(COLLAB_X_OFFSET+50, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) wx.StaticText(p, -1, 'Y-Label', pos=(FORM_WIDTH/2, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) x = FORM_WIDTH/2 + 50 self.txtYLabel = wx.TextCtrl(p, -1, "", size=(100, -1), pos=(x, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) Y_OFFSET += GEN_DELTAY*2.5 self.chkLegend = wx.CheckBox(p, -1, " Show graph legend", pos=(COLLAB_X_OFFSET+20, Y_OFFSET)) self.chkLegendPath = wx.CheckBox(p, -1, " Full file path in legend", pos=(COLLAB_X_OFFSET+180, Y_OFFSET)) Y_OFFSET += GEN_DELTAY*2 font_sizes = ['10', '12', '14', '16', '18', '20', '22', '24', '26', '28', '30', '32'] self.cbLabFontSize = wx.ComboBox(p, -1, "10", (COLLAB_X_OFFSET+20, Y_OFFSET-5), (47, -1), font_sizes, wx.CB_DROPDOWN) wx.StaticText(p, -1, 'Label fontsize', pos=(COLLAB_X_OFFSET+72, Y_OFFSET), style=wx.ALIGN_LEFT) self.chkShowGrid = wx.CheckBox(p, -1, " Show grid", pos=(COLLAB_X_OFFSET+180, Y_OFFSET)) Y_OFFSET += GEN_DELTAY*2 plot_widths = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] self.cbPlotWidth = wx.ComboBox(p, -1, "2", (COLLAB_X_OFFSET+20, Y_OFFSET-5), (47, -1), plot_widths, wx.CB_DROPDOWN) wx.StaticText(p, -1, 'Plot width', pos=(COLLAB_X_OFFSET+72, Y_OFFSET), style=wx.ALIGN_LEFT) Y_OFFSET += GEN_DELTAY + 3 wx.StaticText(p, -1, 'Title', pos=(18, Y_OFFSET+LAB_CTRL_OFFSET+2), style=wx.ALIGN_LEFT) self.txtTitle = wx.TextCtrl(p, -1, "", size=(350, -1), pos=(COLLAB_X_OFFSET+20, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) Y_OFFSET += BUTTON_HEIGHT # + MARGIN x = FORM_WIDTH/2 - BUTTON_WIDTH/2 self.btnPlot = wx.Button(p, label="Plot", pos=(x, Y_OFFSET), size=(100, BUTTON_HEIGHT)) # bind controls/events to handlers self.Bind(wx.EVT_BUTTON, self.AddCSVFile, self.btnAddCSVFile) self.Bind(wx.EVT_BUTTON, self.DelCSVFile, self.btnDelCSVFile) self.Bind(wx.EVT_BUTTON, self.PlotFiles, self.btnPlot) self.Bind(wx.EVT_CHECKBOX, self.ChangeLegend, self.chkLegend) self.Bind(wx.EVT_CHOICE, self.ChangeXLabel, self.cbXColHdr) self.Bind(wx.EVT_CHOICE, self.ChangeYLabel, self.cbYColHdr) self.Bind(wx.EVT_CLOSE, self.doStateSave) # restore saved values self.common_headers = None self.cfg = Config(ConfigFilename) self.doStateRestore() self.updateHdrChoices() def ChangeXLabel(self, event): sel = self.cbXColHdr.GetCurrentSelection() self.txtXLabel.SetValue(self.cbXColHdr.GetItems()[sel].title()) def ChangeYLabel(self, event): sel = self.cbYColHdr.GetCurrentSelection() self.txtYLabel.SetValue(self.cbYColHdr.GetItems()[sel].title()) def AddCSVFile(self, event): """Add a CSV file to the listbox""" dlg = wx.FileDialog(self, message='Choose a CSV file', defaultDir=self.file_dir, defaultFile='', wildcard=CSVWildcard, style=wx.OPEN | wx.MULTIPLE | wx.CHANGE_DIR) if dlg.ShowModal() == wx.ID_OK: # This returns a list of files that were selected. files = dlg.GetFilenames() self.file_dir = dlg.GetDirectory() dlg.Destroy() for f in files: path = os.path.join(self.file_dir, f) if path not in self.CSVFiles: headers = self.getHeaders(path) if headers: common_headers = self.orHeaders(headers) if not common_headers or len(common_headers) < 2: self.error("Sorry, file '%s' doesn't have enough headers in common with current files" % path) else: self.headers.append(headers) self.common_headers = common_headers self.CSVFiles.append(path) self.txtCSVFiles.Append(path) else: self.error("Sorry, file '%s' doesn't appear to be a CSV file" % path) self.updateHdrChoices() def DelCSVFile(self, event): """Delete all CSV files from the listbox""" self.txtCSVFiles.Clear() self.CSVFiles = [] self.common_headers = None self.updateHdrChoices() def ChangeLegend(self, event): if self.chkLegend.GetValue(): self.chkLegendPath.Enable() else: self.chkLegendPath.Disable() def doStateSave(self, event): """Save state from 'self' variables & controls.""" self.cfg['CSVFiles'] = self.CSVFiles self.cfg['XColHdr'] = self.XColHdr self.cfg['XColHdrSelection'] = self.cbXColHdr.GetSelection() self.cfg['YColHdr'] = self.YColHdr self.cfg['YColHdrSelection'] = self.cbYColHdr.GetSelection() self.cfg['file_dir'] = self.file_dir self.cfg['GraphTitle'] = self.txtTitle.GetValue() self.cfg['GraphLegend'] = self.chkLegend.GetValue() self.cfg['LegendPath'] = self.chkLegendPath.GetValue() self.cfg['ShowGrid'] = self.chkShowGrid.GetValue() self.cfg['FontSize'] = self.cbLabFontSize.GetValue() self.cfg['PlotWidth'] = self.cbPlotWidth.GetValue() self.cfg['XLabel'] = self.txtXLabel.GetValue() self.cfg['YLabel'] = self.txtYLabel.GetValue() self.cfg.save() event.Skip(True) def doStateRestore(self): """Restore state to 'self' variables.""" self.CSVFiles = self.cfg['CSVFiles'] if self.CSVFiles is None: self.CSVFiles = [] self.XColHdr = self.cfg['XColHdr'] if self.XColHdr is None: self.XColHdr = [] self.XColHdrSelection = self.cfg['XColHdrSelection'] self.YColHdr = self.cfg['YColHdr'] if self.YColHdr is None: self.YColHdr = [] self.YColHdrSelection = self.cfg['YColHdrSelection'] self.file_dir = self.cfg['file_dir'] if self.file_dir is None: self.file_dir = os.getcwd() # put data into controls self.headers = [] for (i, f) in enumerate(self.CSVFiles): headers = self.getHeaders(f) self.headers.append(headers) self.txtCSVFiles.Insert(f, i) self.common_headers = self.GenCommonHeaders(self.headers) self.updateHdrChoices() if self.XColHdrSelection >= 0: self.cbXColHdr.SetSelection(self.XColHdrSelection) if self.YColHdrSelection >= 0: self.cbYColHdr.SetSelection(self.YColHdrSelection) self.txtTitle.SetValue(self.cfg.get('GraphTitle', '')) self.chkLegend.SetValue(self.cfg.get('GraphLegend', False)) self.chkLegendPath.SetValue(self.cfg.get('LegendPath', False)) self.cbLabFontSize.SetValue(self.cfg.get('FontSize', '14')) self.cbPlotWidth.SetValue(self.cfg.get('PlotWidth', '1')) self.chkShowGrid.SetValue(self.cfg.get('ShowGrid', True)) self.txtXLabel.SetValue(self.cfg.get('XLabel', '')) self.txtYLabel.SetValue(self.cfg.get('YLabel', '')) self.ChangeLegend(None) def getHeaders(self, filename): '''Get header list from a file''' try: fd = open(filename, 'r') hdr = fd.readline() fd.close() except: return None hdr = hdr.split(',') hdr = [x.strip() for x in hdr] return hdr def updateHdrChoices(self): '''Update choice controls with header lists. Disable controls if no CSV files (ie, no headers). Keep selections visible afterwards, if possible. ''' # get current selections, if any selected_x = self.cbXColHdr.GetStringSelection() selected_y = self.cbYColHdr.GetStringSelection() self.cbXColHdr.Clear() self.cbYColHdr.Clear() if self.common_headers: self.cbXColHdr.Enable() self.cbYColHdr.Enable() self.btnPlot.Enable() for h in self.common_headers: self.cbXColHdr.Append(h) self.cbYColHdr.Append(h) if selected_x in self.common_headers: index = self.common_headers.index(selected_x) self.cbXColHdr.SetSelection(index) if selected_y in self.common_headers: index = self.common_headers.index(selected_y) self.cbYColHdr.SetSelection(index) self.txtXLabel.Enable() self.txtYLabel.Enable() else: self.cbXColHdr.Disable() self.cbYColHdr.Disable() self.txtXLabel.Disable() self.txtYLabel.Disable() self.btnPlot.Disable() def orHeaders(self, new_header): '''Update X & Y column header choices. Return new 'common headers' list from current self.common_headers and the supplied new_header list. ''' if self.common_headers: result = [x for x in new_header if x in self.common_headers] else: result = new_header return result def GenCommonHeaders(self, headers): '''Get new set of common headers. Return a new 'common header' list given a list of header lists. ''' result = [] for header in headers: if result: result = [x for x in result if x in header] else: result = header return result def PlotFiles(self, event): '''Plot files in the CSV file listbox.''' selected_x = self.cbXColHdr.GetStringSelection() selected_y = self.cbYColHdr.GetStringSelection() x_label = self.txtXLabel.GetValue() y_label = self.txtYLabel.GetValue() if selected_x and selected_y: grid_on = self.chkShowGrid.GetValue() fontsize = self.cbLabFontSize.GetValue() plot_width = self.cbPlotWidth.GetValue() plot_files(self.CSVFiles, selected_x, selected_y, x_label=x_label, y_label=y_label, title=self.txtTitle.GetValue(), legend=self.chkLegend.GetValue(), legend_path=self.chkLegendPath.GetValue(), grid=grid_on, fontsize=fontsize, plot_width = plot_width) # hide problem with wxPython and matplotlib - close app! self.Close(True) else: self.error('Sorry, you must select X- and Y-column data values') def error(self, msg): '''Issue xwPython error message.''' wx.MessageBox(msg, 'Error') ################################################################################ # Mainline code - start the application. ################################################################################ if __name__ == '__main__': global TheFrame # The frame reference TheFrame = None app = wx.App() TheFrame = MyFrame(None, -1, '%s %s' % (APP_NAME, APP_VERSION), size=(FORM_WIDTH, FORM_HEIGHT)) app.SetTopWindow(TheFrame) app.MainLoop()