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