[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: |
---|
[7200] | 17 | import cpickle as pickle |
---|
[6344] | 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' |
---|
[7200] | 56 | APP_VERSION = '0.7' |
---|
[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 |
---|
[7200] | 75 | BOX_PLOT_HEIGHT = 140+25+25+30 |
---|
[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 |
---|
[7200] | 86 | FORM_HEIGHT = 450+25+25+30 |
---|
[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 | |
---|
[6344] | 163 | ################################################################################ |
---|
| 164 | # An object that behaves like a dictionary but is pickled/unpickled from a file. |
---|
[6477] | 165 | # Used to save state in the application. |
---|
[6344] | 166 | ################################################################################ |
---|
[6338] | 167 | |
---|
[6344] | 168 | class Config(object): |
---|
[6350] | 169 | """An object that behaves like a dictionary but is persistent: |
---|
[6344] | 170 | |
---|
| 171 | cfg = Config('filename') |
---|
| 172 | cfg['save'] = 'A saved value' |
---|
| 173 | cfg[123] = 'value 123' |
---|
| 174 | print cfg['save'] |
---|
| 175 | |
---|
| 176 | The values stored in the file 'filename' will be available to any |
---|
| 177 | application using that config file. |
---|
| 178 | """ |
---|
| 179 | |
---|
| 180 | def __init__(self, configfile=None): |
---|
[6350] | 181 | """__init__(self, String filename=None) -> Config""" |
---|
[6344] | 182 | |
---|
| 183 | self.delconf = False |
---|
| 184 | self.cfgdict = {} |
---|
| 185 | self.changed = False |
---|
| 186 | if not configfile: |
---|
| 187 | self.delconf = True |
---|
| 188 | configfile =tempfile.mktemp() |
---|
| 189 | self.configfile = os.path.abspath(configfile) |
---|
| 190 | if os.path.exists(self.configfile): |
---|
| 191 | try: |
---|
| 192 | f = open(configfile, "r") |
---|
| 193 | u = pickle.Unpickler(f) |
---|
| 194 | self.cfgdict = u.load() |
---|
| 195 | except pickle.UnpicklingError, e: |
---|
| 196 | print e |
---|
| 197 | except: |
---|
| 198 | pass |
---|
| 199 | else: |
---|
| 200 | f.close() |
---|
| 201 | |
---|
| 202 | def __setitem__(self, key, value): |
---|
[6350] | 203 | """Override to allow: cfg[<key>] = <value>""" |
---|
| 204 | |
---|
[6344] | 205 | self.cfgdict[key] = value |
---|
| 206 | self.changed = True |
---|
| 207 | |
---|
[7200] | 208 | def get(self, key, default=None): |
---|
| 209 | """Override to allow: <var> = cfg.get(<key>, default=None)""" |
---|
| 210 | |
---|
| 211 | return self.cfgdict.get(key, default) |
---|
| 212 | |
---|
[6344] | 213 | def __getitem__(self, key): |
---|
[6350] | 214 | """Override to allow: <var> = cfg[<key>]""" |
---|
| 215 | |
---|
[6344] | 216 | return self.cfgdict.get(key, None) |
---|
| 217 | |
---|
| 218 | def __str__(self): |
---|
[6350] | 219 | """__str__(self) -> String""" |
---|
| 220 | |
---|
[6344] | 221 | return "<config object at %s>" % hex(id(self)) |
---|
| 222 | |
---|
| 223 | def getfilename(self): |
---|
[6350] | 224 | """getfilename(self) -> String filename""" |
---|
| 225 | |
---|
[6344] | 226 | return self.configfile |
---|
| 227 | |
---|
| 228 | def setdeleted(self): |
---|
[6350] | 229 | """setdeleted(self)""" |
---|
| 230 | |
---|
[6344] | 231 | self.delconf = True |
---|
| 232 | |
---|
| 233 | def save(self): |
---|
[6350] | 234 | """save(self)""" |
---|
| 235 | |
---|
[6344] | 236 | try: |
---|
| 237 | f = open(self.configfile, "w") |
---|
| 238 | p = pickle.Pickler(f) |
---|
| 239 | p.dump(self.cfgdict) |
---|
| 240 | except pickle.PicklingError, e: |
---|
| 241 | print e |
---|
| 242 | else: |
---|
| 243 | f.close() |
---|
| 244 | self.changed = False |
---|
| 245 | |
---|
| 246 | def close(self): |
---|
[6350] | 247 | """close(self)""" |
---|
| 248 | |
---|
[6344] | 249 | if self.changed: |
---|
| 250 | self.save() |
---|
| 251 | if self.delconf: |
---|
| 252 | if os.path.exists(self.configfile): |
---|
| 253 | os.remove(self.configfile) |
---|
| 254 | |
---|
| 255 | def __del__(self): |
---|
[6350] | 256 | """__del__(self)""" |
---|
| 257 | |
---|
[6344] | 258 | self.close() |
---|
[6350] | 259 | |
---|
[6344] | 260 | |
---|
| 261 | ################################################################################ |
---|
| 262 | # Plot routines |
---|
| 263 | ################################################################################ |
---|
| 264 | |
---|
[6338] | 265 | ## |
---|
[6344] | 266 | # @brief Get CSV data from a file. |
---|
[6338] | 267 | # @param filename Path to the data file to plot. |
---|
[6348] | 268 | # @param x_hdr The X axis title string. |
---|
| 269 | # @param y_hdr The Y axis title string. |
---|
| 270 | def getCSVData(filename, x_hdr, y_hdr): |
---|
[6338] | 271 | # get contents of data file |
---|
| 272 | # after this, 'header' is list of column header strings |
---|
| 273 | # 'data' is a list of lists of data |
---|
| 274 | fd = open(filename) |
---|
| 275 | c = csv.reader(fd) |
---|
| 276 | data = [] |
---|
| 277 | for row in c: |
---|
| 278 | data.append(row) |
---|
| 279 | fd.close() |
---|
| 280 | header = data[0] |
---|
[6344] | 281 | del data[0] # get rid of header in dataset |
---|
[6338] | 282 | |
---|
[6477] | 283 | # ensure header strings don't have leading/trailing spaces |
---|
[6480] | 284 | header = map(string.strip, header) |
---|
[6477] | 285 | |
---|
[6348] | 286 | # get int index values for column headers |
---|
[6338] | 287 | try: |
---|
[6348] | 288 | x_index = header.index(x_hdr) |
---|
| 289 | except ValueError: |
---|
[6482] | 290 | TheFrame.error("Sorry, X column header '%s' isn't in data file '%s'." |
---|
| 291 | % (x_hdr, filename)) |
---|
| 292 | return None |
---|
[6338] | 293 | |
---|
| 294 | try: |
---|
[6348] | 295 | y_index = header.index(y_hdr) |
---|
| 296 | except ValueError: |
---|
[6482] | 297 | TheFrame.error("Sorry, Y column header '%s' isn't in data file '%s'." |
---|
| 298 | % (y_hdr, filename)) |
---|
| 299 | return None |
---|
[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. |
---|
[7200] | 313 | # @param x_hdr The X axis data header string. |
---|
| 314 | # @param y_hdr The Y axis data header 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. |
---|
[7200] | 318 | # @param grid True if grid is to be displayed. |
---|
| 319 | # @param fontsize Point size of font. |
---|
| 320 | # @param plot_width Width of plot lines. |
---|
| 321 | # @param x_label Override X axis label. |
---|
| 322 | # @param y_label Override Y axis label. |
---|
[6358] | 323 | def plot_files(filenames, x_hdr, y_hdr, title='', legend=False, |
---|
[7200] | 324 | legend_path=False, grid=False, fontsize=10, |
---|
| 325 | plot_width=2, x_label=None, y_label=None): |
---|
| 326 | if x_label is None: |
---|
| 327 | x_label = x_hdr |
---|
| 328 | if y_label is None: |
---|
| 329 | y_label = y_hdr |
---|
| 330 | |
---|
[6348] | 331 | pylab.rc('axes', linewidth=2) |
---|
[7200] | 332 | pylab.rc('lines', linewidth=float(plot_width)) |
---|
| 333 | pylab.rc(('xtick', 'ytick'), labelsize=float(fontsize)*0.75,) |
---|
| 334 | pylab.rc(('xtick.major', 'ytick.major'), size=5, pad=5) |
---|
| 335 | pylab.rc('axes', labelsize=float(fontsize)) |
---|
| 336 | pylab.xlabel(x_label) |
---|
| 337 | pylab.ylabel(y_label) |
---|
[6344] | 338 | |
---|
[7200] | 339 | pylab.grid(grid) |
---|
| 340 | |
---|
[6344] | 341 | for f in filenames: |
---|
[6482] | 342 | result = getCSVData(f, x_hdr, y_hdr) |
---|
| 343 | if result is None: # some sort of error |
---|
| 344 | pylab.close() |
---|
| 345 | return |
---|
| 346 | |
---|
| 347 | (x_data, y_data) = result |
---|
[6344] | 348 | pylab.plot(x_data, y_data) |
---|
| 349 | |
---|
[6356] | 350 | if title: |
---|
[7200] | 351 | pylab.title(title, fontsize=float(fontsize)*1.5) |
---|
| 352 | |
---|
[6356] | 353 | if legend: |
---|
[7200] | 354 | pylab.rc('legend', fancybox=True) |
---|
[6356] | 355 | (min_y, max_y) = pylab.ylim() |
---|
| 356 | range_y = max_y - min_y |
---|
| 357 | add_y = float(range_y) / 5 |
---|
| 358 | pylab.ylim((min_y, max_y+add_y)) |
---|
[6358] | 359 | legend_names = filenames |
---|
| 360 | if not legend_path: |
---|
| 361 | legend_names = [os.path.basename(x) for x in filenames] |
---|
| 362 | pylab.legend(legend_names, 'upper right') |
---|
[6348] | 363 | |
---|
[6344] | 364 | pylab.show() |
---|
[6477] | 365 | pylab.close() |
---|
[6344] | 366 | |
---|
| 367 | |
---|
[6351] | 368 | ################################################################################ |
---|
| 369 | # GUI routines |
---|
| 370 | ################################################################################ |
---|
| 371 | |
---|
[6341] | 372 | class MyFrame(wx.Frame): |
---|
| 373 | def __init__(self, parent, id, title, pos=wx.DefaultPosition, |
---|
| 374 | size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE): |
---|
[6351] | 375 | '''Lay out the GUI form''' |
---|
| 376 | |
---|
[6341] | 377 | # Make the frame |
---|
| 378 | wx.Frame.__init__(self, parent, id, title, pos=(50, 50), |
---|
[6482] | 379 | size=(FORM_WIDTH, FORM_HEIGHT), |
---|
| 380 | style=(wx.DEFAULT_FRAME_STYLE & |
---|
| 381 | ~ (wx.RESIZE_BOX | wx.MAXIMIZE_BOX | |
---|
| 382 | wx.RESIZE_BORDER))) |
---|
[6341] | 383 | |
---|
| 384 | p = self.panel = wx.Panel(self, -1) |
---|
| 385 | |
---|
| 386 | if Imported_PyEmbeddedImage: |
---|
| 387 | tsunami = getIcon() |
---|
| 388 | icon = tsunami.GetIcon() |
---|
| 389 | self.SetIcon(icon) |
---|
| 390 | |
---|
| 391 | self.Center() |
---|
| 392 | self.Show(True) |
---|
| 393 | |
---|
| 394 | # start laying out controls |
---|
| 395 | Y_OFFSET = START_YOFFSET |
---|
| 396 | wx.StaticBox(p, -1, 'CSV files', (3, Y_OFFSET), |
---|
| 397 | size=(BOX_CSV_WIDTH, BOX_CSV_HEIGHT)) |
---|
| 398 | Y_OFFSET += GEN_DELTAY |
---|
[6344] | 399 | self.txtCSVFiles = wx.ListBox(p, -1, pos=(8, Y_OFFSET), |
---|
| 400 | size=(TXT_CSVFILE_WIDTH, |
---|
| 401 | TXT_CSVFILE_HEIGHT)) |
---|
| 402 | self.CSVFiles = [] |
---|
[6341] | 403 | Y_OFFSET += TXT_CSVFILE_HEIGHT + MARGIN |
---|
| 404 | x = FORM_WIDTH/2 - BUTTON_WIDTH - DOUBLE_BUTTON_OFFSET |
---|
| 405 | self.btnAddCSVFile = wx.Button(p, label="Add", pos=(x, Y_OFFSET), |
---|
| 406 | size=(100, BUTTON_HEIGHT)) |
---|
| 407 | x = FORM_WIDTH/2 + DOUBLE_BUTTON_OFFSET |
---|
[7200] | 408 | self.btnDelCSVFile = wx.Button(p, label="Delete All", pos=(x, Y_OFFSET), |
---|
[6341] | 409 | size=(100, BUTTON_HEIGHT)) |
---|
| 410 | Y_OFFSET += BUTTON_HEIGHT + MARGIN |
---|
| 411 | |
---|
[6348] | 412 | wx.StaticBox(p, -1, 'Plot files', (3, Y_OFFSET), |
---|
[6341] | 413 | size=(BOX_PLOT_WIDTH, BOX_PLOT_HEIGHT)) |
---|
| 414 | Y_OFFSET += GEN_DELTAY |
---|
[7200] | 415 | |
---|
[6341] | 416 | wx.StaticText(p, -1, 'X-Column', |
---|
| 417 | pos=(COLLAB_X_OFFSET, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) |
---|
[7200] | 418 | self.cbXColHdr = wx.Choice(p, -1, pos=(COLLAB_X_OFFSET+50, Y_OFFSET), |
---|
[6344] | 419 | size=(80, -1)) |
---|
| 420 | self.XColHdr = [] |
---|
[6341] | 421 | wx.StaticText(p, -1, 'Y-Column', |
---|
[7200] | 422 | pos=(FORM_WIDTH/2, |
---|
[6341] | 423 | Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) |
---|
[7200] | 424 | x = FORM_WIDTH/2+50 |
---|
| 425 | self.cbYColHdr = wx.Choice(p, -1, pos=(x, Y_OFFSET), size=(80, -1)) |
---|
[6344] | 426 | self.YColHdr = [] |
---|
[7200] | 427 | Y_OFFSET += GEN_DELTAY*1.5 |
---|
[6341] | 428 | |
---|
[7200] | 429 | wx.StaticText(p, -1, 'X-Label', |
---|
| 430 | pos=(COLLAB_X_OFFSET, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) |
---|
| 431 | self.txtXLabel = wx.TextCtrl(p, -1, "", size=(100, -1), |
---|
| 432 | pos=(COLLAB_X_OFFSET+50, |
---|
| 433 | Y_OFFSET+LAB_CTRL_OFFSET), |
---|
| 434 | style=wx.ALIGN_LEFT) |
---|
| 435 | wx.StaticText(p, -1, 'Y-Label', |
---|
| 436 | pos=(FORM_WIDTH/2, |
---|
| 437 | Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT) |
---|
| 438 | x = FORM_WIDTH/2 + 50 |
---|
| 439 | self.txtYLabel = wx.TextCtrl(p, -1, "", size=(100, -1), |
---|
| 440 | pos=(x, |
---|
| 441 | Y_OFFSET+LAB_CTRL_OFFSET), |
---|
| 442 | style=wx.ALIGN_LEFT) |
---|
| 443 | Y_OFFSET += GEN_DELTAY*2.5 |
---|
| 444 | |
---|
[6356] | 445 | self.chkLegend = wx.CheckBox(p, -1, " Show graph legend", |
---|
[6358] | 446 | pos=(COLLAB_X_OFFSET+20, Y_OFFSET)) |
---|
| 447 | self.chkLegendPath = wx.CheckBox(p, -1, " Full file path in legend", |
---|
| 448 | pos=(COLLAB_X_OFFSET+180, Y_OFFSET)) |
---|
[7200] | 449 | Y_OFFSET += GEN_DELTAY*2 |
---|
| 450 | |
---|
| 451 | font_sizes = ['10', '12', '14', '16', '18', '20', |
---|
| 452 | '22', '24', '26', '28', '30', '32'] |
---|
| 453 | self.cbLabFontSize = wx.ComboBox(p, -1, "10", |
---|
| 454 | (COLLAB_X_OFFSET+20, Y_OFFSET-5), |
---|
| 455 | (47, -1), font_sizes, |
---|
| 456 | wx.CB_DROPDOWN) |
---|
| 457 | wx.StaticText(p, -1, 'Label fontsize', |
---|
| 458 | pos=(COLLAB_X_OFFSET+72, Y_OFFSET), |
---|
| 459 | style=wx.ALIGN_LEFT) |
---|
| 460 | |
---|
| 461 | self.chkShowGrid = wx.CheckBox(p, -1, " Show grid", |
---|
| 462 | pos=(COLLAB_X_OFFSET+180, Y_OFFSET)) |
---|
| 463 | |
---|
| 464 | Y_OFFSET += GEN_DELTAY*2 |
---|
| 465 | |
---|
| 466 | plot_widths = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] |
---|
| 467 | self.cbPlotWidth = wx.ComboBox(p, -1, "2", |
---|
| 468 | (COLLAB_X_OFFSET+20, Y_OFFSET-5), |
---|
| 469 | (47, -1), plot_widths, |
---|
| 470 | wx.CB_DROPDOWN) |
---|
| 471 | wx.StaticText(p, -1, 'Plot width', |
---|
| 472 | pos=(COLLAB_X_OFFSET+72, Y_OFFSET), |
---|
| 473 | style=wx.ALIGN_LEFT) |
---|
[6358] | 474 | Y_OFFSET += GEN_DELTAY + 3 |
---|
[6356] | 475 | |
---|
| 476 | wx.StaticText(p, -1, 'Title', |
---|
| 477 | pos=(18, Y_OFFSET+LAB_CTRL_OFFSET+2), style=wx.ALIGN_LEFT) |
---|
| 478 | self.txtTitle = wx.TextCtrl(p, -1, "", size=(350, -1), |
---|
| 479 | pos=(COLLAB_X_OFFSET+20, |
---|
| 480 | Y_OFFSET+LAB_CTRL_OFFSET), |
---|
| 481 | style=wx.ALIGN_LEFT) |
---|
[7200] | 482 | Y_OFFSET += BUTTON_HEIGHT # + MARGIN |
---|
[6341] | 483 | x = FORM_WIDTH/2 - BUTTON_WIDTH/2 |
---|
[6344] | 484 | self.btnPlot = wx.Button(p, label="Plot", pos=(x, Y_OFFSET), |
---|
[6341] | 485 | size=(100, BUTTON_HEIGHT)) |
---|
| 486 | |
---|
[6351] | 487 | # bind controls/events to handlers |
---|
[6344] | 488 | self.Bind(wx.EVT_BUTTON, self.AddCSVFile, self.btnAddCSVFile) |
---|
| 489 | self.Bind(wx.EVT_BUTTON, self.DelCSVFile, self.btnDelCSVFile) |
---|
| 490 | self.Bind(wx.EVT_BUTTON, self.PlotFiles, self.btnPlot) |
---|
[6358] | 491 | self.Bind(wx.EVT_CHECKBOX, self.ChangeLegend, self.chkLegend) |
---|
[7200] | 492 | self.Bind(wx.EVT_CHOICE, self.ChangeXLabel, self.cbXColHdr) |
---|
| 493 | self.Bind(wx.EVT_CHOICE, self.ChangeYLabel, self.cbYColHdr) |
---|
[6351] | 494 | |
---|
[6344] | 495 | self.Bind(wx.EVT_CLOSE, self.doStateSave) |
---|
[6351] | 496 | |
---|
| 497 | # restore saved values |
---|
| 498 | self.common_headers = None |
---|
[6344] | 499 | self.cfg = Config(ConfigFilename) |
---|
| 500 | self.doStateRestore() |
---|
| 501 | self.updateHdrChoices() |
---|
[7200] | 502 | |
---|
| 503 | def ChangeXLabel(self, event): |
---|
| 504 | sel = self.cbXColHdr.GetCurrentSelection() |
---|
| 505 | self.txtXLabel.SetValue(self.cbXColHdr.GetItems()[sel].title()) |
---|
| 506 | |
---|
| 507 | def ChangeYLabel(self, event): |
---|
| 508 | sel = self.cbYColHdr.GetCurrentSelection() |
---|
| 509 | self.txtYLabel.SetValue(self.cbYColHdr.GetItems()[sel].title()) |
---|
| 510 | |
---|
[6344] | 511 | def AddCSVFile(self, event): |
---|
| 512 | """Add a CSV file to the listbox""" |
---|
[6341] | 513 | |
---|
[6344] | 514 | dlg = wx.FileDialog(self, message='Choose a CSV file', |
---|
| 515 | defaultDir=self.file_dir, |
---|
| 516 | defaultFile='', |
---|
| 517 | wildcard=CSVWildcard, |
---|
[7200] | 518 | style=wx.OPEN | wx.MULTIPLE | wx.CHANGE_DIR) |
---|
[6341] | 519 | |
---|
[6344] | 520 | if dlg.ShowModal() == wx.ID_OK: |
---|
| 521 | # This returns a list of files that were selected. |
---|
[7200] | 522 | files = dlg.GetFilenames() |
---|
[6344] | 523 | self.file_dir = dlg.GetDirectory() |
---|
| 524 | dlg.Destroy() |
---|
[7200] | 525 | for f in files: |
---|
| 526 | path = os.path.join(self.file_dir, f) |
---|
| 527 | if path not in self.CSVFiles: |
---|
| 528 | headers = self.getHeaders(path) |
---|
| 529 | if headers: |
---|
| 530 | common_headers = self.orHeaders(headers) |
---|
| 531 | if not common_headers or len(common_headers) < 2: |
---|
| 532 | self.error("Sorry, file '%s' doesn't have enough headers in common with current files" % path) |
---|
| 533 | else: |
---|
| 534 | self.headers.append(headers) |
---|
| 535 | self.common_headers = common_headers |
---|
| 536 | self.CSVFiles.append(path) |
---|
| 537 | self.txtCSVFiles.Append(path) |
---|
[6344] | 538 | else: |
---|
[7200] | 539 | self.error("Sorry, file '%s' doesn't appear to be a CSV file" % path) |
---|
| 540 | self.updateHdrChoices() |
---|
[6341] | 541 | |
---|
[6344] | 542 | def DelCSVFile(self, event): |
---|
[7200] | 543 | """Delete all CSV files from the listbox""" |
---|
| 544 | |
---|
| 545 | self.txtCSVFiles.Clear() |
---|
| 546 | self.CSVFiles = [] |
---|
| 547 | self.common_headers = None |
---|
| 548 | self.updateHdrChoices() |
---|
| 549 | |
---|
[6358] | 550 | def ChangeLegend(self, event): |
---|
| 551 | if self.chkLegend.GetValue(): |
---|
| 552 | self.chkLegendPath.Enable() |
---|
| 553 | else: |
---|
| 554 | self.chkLegendPath.Disable() |
---|
| 555 | |
---|
[6344] | 556 | def doStateSave(self, event): |
---|
[6351] | 557 | """Save state from 'self' variables & controls.""" |
---|
[6341] | 558 | |
---|
[6344] | 559 | self.cfg['CSVFiles'] = self.CSVFiles |
---|
| 560 | self.cfg['XColHdr'] = self.XColHdr |
---|
| 561 | self.cfg['XColHdrSelection'] = self.cbXColHdr.GetSelection() |
---|
| 562 | self.cfg['YColHdr'] = self.YColHdr |
---|
| 563 | self.cfg['YColHdrSelection'] = self.cbYColHdr.GetSelection() |
---|
| 564 | self.cfg['file_dir'] = self.file_dir |
---|
[6356] | 565 | self.cfg['GraphTitle'] = self.txtTitle.GetValue() |
---|
| 566 | self.cfg['GraphLegend'] = self.chkLegend.GetValue() |
---|
[6358] | 567 | self.cfg['LegendPath'] = self.chkLegendPath.GetValue() |
---|
[7200] | 568 | self.cfg['ShowGrid'] = self.chkShowGrid.GetValue() |
---|
| 569 | self.cfg['FontSize'] = self.cbLabFontSize.GetValue() |
---|
| 570 | self.cfg['PlotWidth'] = self.cbPlotWidth.GetValue() |
---|
| 571 | self.cfg['XLabel'] = self.txtXLabel.GetValue() |
---|
| 572 | self.cfg['YLabel'] = self.txtYLabel.GetValue() |
---|
[6341] | 573 | |
---|
[6344] | 574 | self.cfg.save() |
---|
| 575 | event.Skip(True) |
---|
[6341] | 576 | |
---|
[6344] | 577 | def doStateRestore(self): |
---|
| 578 | """Restore state to 'self' variables.""" |
---|
[6341] | 579 | |
---|
[6344] | 580 | self.CSVFiles = self.cfg['CSVFiles'] |
---|
| 581 | if self.CSVFiles is None: |
---|
| 582 | self.CSVFiles = [] |
---|
| 583 | self.XColHdr = self.cfg['XColHdr'] |
---|
| 584 | if self.XColHdr is None: |
---|
| 585 | self.XColHdr = [] |
---|
[6348] | 586 | self.XColHdrSelection = self.cfg['XColHdrSelection'] |
---|
[6344] | 587 | self.YColHdr = self.cfg['YColHdr'] |
---|
| 588 | if self.YColHdr is None: |
---|
| 589 | self.YColHdr = [] |
---|
[6348] | 590 | self.YColHdrSelection = self.cfg['YColHdrSelection'] |
---|
[6344] | 591 | self.file_dir = self.cfg['file_dir'] |
---|
| 592 | if self.file_dir is None: |
---|
| 593 | self.file_dir = os.getcwd() |
---|
[6341] | 594 | |
---|
[6344] | 595 | # put data into controls |
---|
| 596 | self.headers = [] |
---|
| 597 | for (i, f) in enumerate(self.CSVFiles): |
---|
| 598 | headers = self.getHeaders(f) |
---|
| 599 | self.headers.append(headers) |
---|
| 600 | self.txtCSVFiles.Insert(f, i) |
---|
[6348] | 601 | self.common_headers = self.GenCommonHeaders(self.headers) |
---|
| 602 | self.updateHdrChoices() |
---|
[6338] | 603 | |
---|
[6352] | 604 | if self.XColHdrSelection >= 0: |
---|
[6348] | 605 | self.cbXColHdr.SetSelection(self.XColHdrSelection) |
---|
[6352] | 606 | if self.YColHdrSelection >= 0: |
---|
[6348] | 607 | self.cbYColHdr.SetSelection(self.YColHdrSelection) |
---|
[7200] | 608 | self.txtTitle.SetValue(self.cfg.get('GraphTitle', '')) |
---|
| 609 | self.chkLegend.SetValue(self.cfg.get('GraphLegend', False)) |
---|
| 610 | self.chkLegendPath.SetValue(self.cfg.get('LegendPath', False)) |
---|
| 611 | self.cbLabFontSize.SetValue(self.cfg.get('FontSize', '14')) |
---|
| 612 | self.cbPlotWidth.SetValue(self.cfg.get('PlotWidth', '1')) |
---|
| 613 | self.chkShowGrid.SetValue(self.cfg.get('ShowGrid', True)) |
---|
| 614 | self.txtXLabel.SetValue(self.cfg.get('XLabel', '')) |
---|
| 615 | self.txtYLabel.SetValue(self.cfg.get('YLabel', '')) |
---|
[6358] | 616 | self.ChangeLegend(None) |
---|
| 617 | |
---|
[6344] | 618 | def getHeaders(self, filename): |
---|
| 619 | '''Get header list from a file''' |
---|
[6338] | 620 | |
---|
[6344] | 621 | try: |
---|
| 622 | fd = open(filename, 'r') |
---|
| 623 | hdr = fd.readline() |
---|
| 624 | fd.close() |
---|
| 625 | except: |
---|
| 626 | return None |
---|
| 627 | |
---|
| 628 | hdr = hdr.split(',') |
---|
| 629 | hdr = [x.strip() for x in hdr] |
---|
| 630 | |
---|
| 631 | return hdr |
---|
[6341] | 632 | |
---|
[6344] | 633 | def updateHdrChoices(self): |
---|
| 634 | '''Update choice controls with header lists. |
---|
[6338] | 635 | |
---|
[6351] | 636 | Disable controls if no CSV files (ie, no headers). |
---|
| 637 | Keep selections visible afterwards, if possible. |
---|
[6344] | 638 | ''' |
---|
| 639 | |
---|
[6351] | 640 | # get current selections, if any |
---|
[6344] | 641 | selected_x = self.cbXColHdr.GetStringSelection() |
---|
| 642 | selected_y = self.cbYColHdr.GetStringSelection() |
---|
[6341] | 643 | |
---|
[6344] | 644 | self.cbXColHdr.Clear() |
---|
| 645 | self.cbYColHdr.Clear() |
---|
| 646 | |
---|
| 647 | if self.common_headers: |
---|
| 648 | self.cbXColHdr.Enable() |
---|
| 649 | self.cbYColHdr.Enable() |
---|
| 650 | self.btnPlot.Enable() |
---|
| 651 | for h in self.common_headers: |
---|
| 652 | self.cbXColHdr.Append(h) |
---|
| 653 | self.cbYColHdr.Append(h) |
---|
| 654 | if selected_x in self.common_headers: |
---|
| 655 | index = self.common_headers.index(selected_x) |
---|
| 656 | self.cbXColHdr.SetSelection(index) |
---|
| 657 | if selected_y in self.common_headers: |
---|
| 658 | index = self.common_headers.index(selected_y) |
---|
| 659 | self.cbYColHdr.SetSelection(index) |
---|
[7200] | 660 | self.txtXLabel.Enable() |
---|
| 661 | self.txtYLabel.Enable() |
---|
[6344] | 662 | else: |
---|
| 663 | self.cbXColHdr.Disable() |
---|
| 664 | self.cbYColHdr.Disable() |
---|
[7200] | 665 | self.txtXLabel.Disable() |
---|
| 666 | self.txtYLabel.Disable() |
---|
[6344] | 667 | self.btnPlot.Disable() |
---|
| 668 | |
---|
| 669 | def orHeaders(self, new_header): |
---|
[6351] | 670 | '''Update X & Y column header choices. |
---|
[6344] | 671 | |
---|
[6351] | 672 | Return new 'common headers' list from current |
---|
| 673 | self.common_headers and the supplied new_header list. |
---|
| 674 | ''' |
---|
| 675 | |
---|
[6344] | 676 | if self.common_headers: |
---|
| 677 | result = [x for x in new_header if x in self.common_headers] |
---|
| 678 | else: |
---|
| 679 | result = new_header |
---|
| 680 | return result |
---|
| 681 | |
---|
| 682 | def GenCommonHeaders(self, headers): |
---|
[6351] | 683 | '''Get new set of common headers. |
---|
[6344] | 684 | |
---|
[6351] | 685 | Return a new 'common header' list given a |
---|
| 686 | list of header lists. |
---|
| 687 | ''' |
---|
| 688 | |
---|
[6344] | 689 | result = [] |
---|
| 690 | for header in headers: |
---|
| 691 | if result: |
---|
| 692 | result = [x for x in result if x in header] |
---|
| 693 | else: |
---|
| 694 | result = header |
---|
| 695 | return result |
---|
| 696 | |
---|
| 697 | def PlotFiles(self, event): |
---|
[6351] | 698 | '''Plot files in the CSV file listbox.''' |
---|
| 699 | |
---|
[6344] | 700 | selected_x = self.cbXColHdr.GetStringSelection() |
---|
| 701 | selected_y = self.cbYColHdr.GetStringSelection() |
---|
| 702 | |
---|
[7200] | 703 | x_label = self.txtXLabel.GetValue() |
---|
| 704 | y_label = self.txtYLabel.GetValue() |
---|
| 705 | |
---|
[6344] | 706 | if selected_x and selected_y: |
---|
[7200] | 707 | grid_on = self.chkShowGrid.GetValue() |
---|
| 708 | fontsize = self.cbLabFontSize.GetValue() |
---|
| 709 | plot_width = self.cbPlotWidth.GetValue() |
---|
[6356] | 710 | plot_files(self.CSVFiles, selected_x, selected_y, |
---|
[7200] | 711 | x_label=x_label, y_label=y_label, |
---|
[6356] | 712 | title=self.txtTitle.GetValue(), |
---|
[6358] | 713 | legend=self.chkLegend.GetValue(), |
---|
[7200] | 714 | legend_path=self.chkLegendPath.GetValue(), |
---|
| 715 | grid=grid_on, fontsize=fontsize, |
---|
| 716 | plot_width = plot_width) |
---|
| 717 | |
---|
[6906] | 718 | # hide problem with wxPython and matplotlib - close app! |
---|
| 719 | self.Close(True) |
---|
[7200] | 720 | else: |
---|
| 721 | self.error('Sorry, you must select X- and Y-column data values') |
---|
[6344] | 722 | |
---|
[6341] | 723 | def error(self, msg): |
---|
[6351] | 724 | '''Issue xwPython error message.''' |
---|
| 725 | |
---|
[6482] | 726 | wx.MessageBox(msg, 'Error') |
---|
[6338] | 727 | |
---|
[6341] | 728 | |
---|
| 729 | ################################################################################ |
---|
[6351] | 730 | # Mainline code - start the application. |
---|
[6341] | 731 | ################################################################################ |
---|
[6338] | 732 | |
---|
[6341] | 733 | if __name__ == '__main__': |
---|
[6482] | 734 | global TheFrame |
---|
| 735 | |
---|
| 736 | # The frame reference |
---|
| 737 | TheFrame = None |
---|
| 738 | |
---|
[6341] | 739 | app = wx.App() |
---|
[6482] | 740 | TheFrame = MyFrame(None, -1, '%s %s' % (APP_NAME, APP_VERSION), |
---|
| 741 | size=(FORM_WIDTH, FORM_HEIGHT)) |
---|
| 742 | app.SetTopWindow(TheFrame) |
---|
[6341] | 743 | app.MainLoop() |
---|
[6338] | 744 | |
---|