Package PyFoam :: Package ThirdParty :: Package Gnuplot :: Module PlotItems
[hide private]
[frames] | no frames]

Source Code for Module PyFoam.ThirdParty.Gnuplot.PlotItems

  1  # $Id: PlotItems.py 299 2007-03-30 12:52:17Z mhagger $ 
  2   
  3  # Copyright (C) 1998-2003 Michael Haggerty <mhagger@alum.mit.edu> 
  4  # 
  5  # This file is licensed under the GNU Lesser General Public License 
  6  # (LGPL).  See LICENSE.txt for details. 
  7   
  8  """PlotItems.py -- Objects that can be plotted by Gnuplot. 
  9   
 10  This module contains several types of PlotItems.  PlotItems can be 
 11  plotted by passing them to a Gnuplot.Gnuplot object.  You can derive 
 12  your own classes from the PlotItem hierarchy to customize their 
 13  behavior. 
 14   
 15  """ 
 16   
 17  import os, string, tempfile, types 
 18   
 19  try: 
 20      from cStringIO import StringIO 
 21  except ImportError: 
 22      from StringIO import StringIO 
 23   
 24  import numpy 
 25       
 26  import gp, utils, Errors 
 27   
 28   
29 -class _unset:
30 """Used to represent unset keyword arguments.""" 31 32 pass
33 34
35 -class PlotItem:
36 """Plotitem represents an item that can be plotted by gnuplot. 37 38 For the finest control over the output, you can create 'PlotItems' 39 yourself with additional keyword options, or derive new classes 40 from 'PlotItem'. 41 42 The handling of options is complicated by the attempt to allow 43 options and their setting mechanism to be inherited conveniently. 44 Note first that there are some options that can only be set in the 45 constructor then never modified, and others that can be set in the 46 constructor and/or modified using the 'set_option()' member 47 function. The former are always processed within '__init__'. The 48 latter are always processed within 'set_option', which is called 49 by the constructor. 50 51 'set_option' is driven by a class-wide dictionary called 52 '_option_list', which is a mapping '{ <option> : <setter> }' from 53 option name to the function object used to set or change the 54 option. <setter> is a function object that takes two parameters: 55 'self' (the 'PlotItem' instance) and the new value requested for 56 the option. If <setter> is 'None', then the option is not allowed 57 to be changed after construction and an exception is raised. 58 59 Any 'PlotItem' that needs to add options can add to this 60 dictionary within its class definition. Follow one of the 61 examples in this file. Alternatively it could override the 62 'set_option' member function if it needs to do wilder things. 63 64 Members: 65 66 '_basecommand' -- a string holding the elementary argument that 67 must be passed to gnuplot's `plot' command for this item; 68 e.g., 'sin(x)' or '"filename.dat"'. 69 70 '_options' -- a dictionary of (<option>,<string>) tuples 71 corresponding to the plot options that have been set for 72 this instance of the PlotItem. <option> is the option as 73 specified by the user; <string> is the string that needs to 74 be set in the command line to set that option (or None if no 75 string is needed). Example:: 76 77 {'title' : ('Data', 'title "Data"'), 78 'with' : ('linespoints', 'with linespoints')} 79 80 """ 81 82 # For _option_list explanation, see docstring for PlotItem. 83 _option_list = { 84 'axes' : lambda self, axes: self.set_string_option( 85 'axes', axes, None, 'axes %s'), 86 'with' : lambda self, with_: self.set_string_option( 87 'with', with_, None, 'with %s'), 88 'title' : lambda self, title: self.set_string_option( 89 'title', title, 'notitle', 'title "%s"'), 90 } 91 _option_list['with_'] = _option_list['with'] 92 93 # order in which options need to be passed to gnuplot: 94 _option_sequence = [ 95 'binary', 96 'index', 'every', 'thru', 'using', 'smooth', 97 'axes', 'title', 'with' 98 ] 99
100 - def __init__(self, **keyw):
101 """Construct a 'PlotItem'. 102 103 Keyword options: 104 105 'with_=<string>' -- choose how item will be plotted, e.g., 106 with_='points 3 3'. 107 108 'title=<string>' -- set the title to be associated with the item 109 in the plot legend. 110 111 'title=None' -- choose 'notitle' option (omit item from legend). 112 113 Note that omitting the title option is different than setting 114 'title=None'; the former chooses gnuplot's default whereas the 115 latter chooses 'notitle'. 116 117 """ 118 119 self._options = {} 120 self.set_option(**keyw)
121
122 - def get_option(self, name):
123 """Return the setting of an option. May be overridden.""" 124 125 try: 126 return self._options[name][0] 127 except: 128 raise KeyError('option %s is not set!' % name)
129
130 - def set_option(self, **keyw):
131 """Set or change a plot option for this PlotItem. 132 133 See documentation for '__init__' for information about allowed 134 options. This function can be overridden by derived classes 135 to allow additional options, in which case those options will 136 also be allowed by '__init__' for the derived class. However, 137 it is easier to define a new '_option_list' variable for the 138 derived class. 139 140 """ 141 142 for (option, value) in keyw.items(): 143 try: 144 setter = self._option_list[option] 145 except KeyError: 146 raise Errors.OptionError('%s=%s' % (option,value)) 147 if setter is None: 148 raise Errors.OptionError( 149 'Cannot modify %s option after construction!', option) 150 else: 151 setter(self, value)
152
153 - def set_string_option(self, option, value, default, fmt):
154 """Set an option that takes a string value.""" 155 156 if value is None: 157 self._options[option] = (value, default) 158 elif type(value) is types.StringType: 159 self._options[option] = (value, fmt % value) 160 else: 161 Errors.OptionError('%s=%s' % (option, value,))
162
163 - def clear_option(self, name):
164 """Clear (unset) a plot option. No error if option was not set.""" 165 166 try: 167 del self._options[name] 168 except KeyError: 169 pass
170
171 - def get_base_command_string(self):
172 raise NotImplementedError()
173
175 cmd = [] 176 for opt in self._option_sequence: 177 (val,str) = self._options.get(opt, (None,None)) 178 if str is not None: 179 cmd.append(str) 180 return string.join(cmd)
181
182 - def command(self):
183 """Build the plot command to be sent to gnuplot. 184 185 Build and return the plot command, with options, necessary to 186 display this item. If anything else needs to be done once per 187 plot, it can be done here too. 188 189 """ 190 191 return string.join([ 192 self.get_base_command_string(), 193 self.get_command_option_string(), 194 ])
195
196 - def pipein(self, f):
197 """Pipe necessary inline data to gnuplot. 198 199 If the plot command requires data to be put on stdin (i.e., 200 'plot "-"'), this method should put that data there. Can be 201 overridden in derived classes. 202 203 """ 204 205 pass
206 207
208 -class Func(PlotItem):
209 """Represents a mathematical expression to plot. 210 211 Func represents a mathematical expression that is to be computed by 212 gnuplot itself, as if you would type for example:: 213 214 gnuplot> plot sin(x) 215 216 into gnuplot itself. The argument to the contructor is a string 217 that should be a mathematical expression. Example:: 218 219 g.plot(Func('sin(x)', with_='line 3')) 220 221 As shorthand, a string passed to the plot method of a Gnuplot 222 object is also treated as a Func:: 223 224 g.plot('sin(x)') 225 226 """ 227
228 - def __init__(self, function, **keyw):
229 PlotItem.__init__(self, **keyw) 230 self.function = function
231
232 - def get_base_command_string(self):
233 return self.function
234 235
236 -class _FileItem(PlotItem):
237 """A PlotItem representing a file that contains gnuplot data. 238 239 This class is not meant for users but rather as a base class for 240 other types of FileItem. 241 242 """ 243 244 _option_list = PlotItem._option_list.copy() 245 _option_list.update({ 246 'binary' : lambda self, binary: self.set_option_binary(binary), 247 'index' : lambda self, value: self.set_option_colonsep('index', value), 248 'every' : lambda self, value: self.set_option_colonsep('every', value), 249 'using' : lambda self, value: self.set_option_colonsep('using', value), 250 'smooth' : lambda self, smooth: self.set_string_option( 251 'smooth', smooth, None, 'smooth %s' 252 ), 253 }) 254
255 - def __init__(self, filename, **keyw):
256 """Represent a PlotItem that gnuplot treates as a file. 257 258 This class holds the information that is needed to construct 259 the plot command line, including options that are specific to 260 file-like gnuplot input. 261 262 <filename> is a string representing the filename to be passed 263 to gnuplot within quotes. It may be the name of an existing 264 file, '-' for inline data, or the name of a named pipe. 265 266 Keyword arguments: 267 268 'using=<int>' -- plot that column against line number 269 270 'using=<tuple>' -- plot using a:b:c:d etc. Elements in 271 the tuple that are None are output as the empty 272 string. 273 274 'using=<string>' -- plot `using <string>' (allows gnuplot's 275 arbitrary column arithmetic) 276 277 'every=<value>' -- plot 'every <value>'. <value> is 278 formatted as for 'using' option. 279 280 'index=<value>' -- plot 'index <value>'. <value> is 281 formatted as for 'using' option. 282 283 'binary=<boolean>' -- data in the file is in binary format 284 (this option is only allowed for grid data for splot). 285 286 'smooth=<string>' -- smooth the data. Option should be 287 'unique', 'csplines', 'acsplines', 'bezier', or 288 'sbezier'. 289 290 The keyword arguments recognized by 'PlotItem' can also be 291 used here. 292 293 Note that the 'using' option is interpreted by gnuplot, so 294 columns must be numbered starting with 1. 295 296 By default, gnuplot uses the name of the file plus any 'using' 297 option as the dataset title. If you want another title, set 298 it explicitly using the 'title' option. 299 300 """ 301 302 self.filename = filename 303 304 PlotItem.__init__(self, **keyw)
305
306 - def get_base_command_string(self):
307 return gp.double_quote_string(self.filename)
308
309 - def set_option_colonsep(self, name, value):
310 if value is None: 311 self.clear_option(name) 312 elif type(value) in [types.StringType, types.IntType]: 313 self._options[name] = (value, '%s %s' % (name, value,)) 314 elif type(value) is types.TupleType: 315 subopts = [] 316 for subopt in value: 317 if subopt is None: 318 subopts.append('') 319 else: 320 subopts.append(str(subopt)) 321 self._options[name] = ( 322 value, 323 '%s %s' % (name, string.join(subopts, ':'),), 324 ) 325 else: 326 raise Errors.OptionError('%s=%s' % (name, value,))
327
328 - def set_option_binary(self, binary):
329 if binary: 330 if not gp.GnuplotOpts.recognizes_binary_splot: 331 raise Errors.OptionError( 332 'Gnuplot.py is currently configured to reject binary data') 333 self._options['binary'] = (1, 'binary') 334 else: 335 self._options['binary'] = (0, None)
336 337
338 -class _NewFileItem(_FileItem):
339 - def __init__(self, content, filename=None, **keyw):
340 341 binary = keyw.get('binary', 0) 342 if binary: 343 mode = 'wb' 344 else: 345 mode = 'w' 346 347 if filename: 348 # This is a permanent file 349 self.temp = False 350 f = open(filename, mode) 351 else: 352 self.temp = True 353 if hasattr(tempfile, 'mkstemp'): 354 # Use the new secure method of creating temporary files: 355 (fd, filename,) = tempfile.mkstemp( 356 suffix='.gnuplot', text=(not binary) 357 ) 358 f = os.fdopen(fd, mode) 359 else: 360 # for backwards compatibility to pre-2.3: 361 filename = tempfile.mktemp() 362 f = open(filename, mode) 363 364 f.write(content) 365 f.close() 366 367 # If the user hasn't specified a title, set it to None so 368 # that the name of the temporary file is not used: 369 if self.temp and 'title' not in keyw: 370 keyw['title'] = None 371 372 _FileItem.__init__(self, filename, **keyw)
373
374 - def __del__(self):
375 if self.temp: 376 os.unlink(self.filename)
377 378
379 -class _InlineFileItem(_FileItem):
380 """A _FileItem that actually indicates inline data. 381 382 """ 383
384 - def __init__(self, content, **keyw):
385 # If the user hasn't specified a title, set it to None so that 386 # '-' is not used: 387 if 'title' not in keyw: 388 keyw['title'] = None 389 390 if keyw.get('binary', 0): 391 raise Errors.OptionError('binary inline data is not supported') 392 393 _FileItem.__init__(self, '-', **keyw) 394 395 if content[-1] == '\n': 396 self.content = content 397 else: 398 self.content = content + '\n'
399
400 - def pipein(self, f):
401 f.write(self.content + 'e\n')
402 403 404 if gp.GnuplotOpts.support_fifo: 405 import threading 406
407 - class _FIFOWriter(threading.Thread):
408 """Create a FIFO (named pipe), write to it, then delete it. 409 410 The writing takes place in a separate thread so that the main 411 thread is not blocked. The idea is that once the writing is 412 finished we know that gnuplot is done with the data that were in 413 the file so we can delete the file. This technique removes the 414 ambiguity about when the temporary files should be deleted. 415 416 Since the tempfile module does not provide an easy, secure way 417 to create a FIFO without race conditions, we instead create a 418 temporary directory using mkdtemp() then create the FIFO 419 within that directory. When the writer thread has written the 420 full information to the FIFO, it deletes both the FIFO and the 421 temporary directory that contained it. 422 423 """ 424
425 - def __init__(self, content, mode='w'):
426 self.content = content 427 self.mode = mode 428 if hasattr(tempfile, 'mkdtemp'): 429 # Make the file within a temporary directory that is 430 # created securely: 431 self.dirname = tempfile.mkdtemp(suffix='.gnuplot') 432 self.filename = os.path.join(self.dirname, 'fifo') 433 else: 434 # For backwards compatibility pre-2.3, just use 435 # mktemp() to create filename: 436 self.dirname = None 437 self.filename = tempfile.mktemp() 438 threading.Thread.__init__( 439 self, 440 name=('FIFO Writer for %s' % (self.filename,)), 441 ) 442 os.mkfifo(self.filename) 443 self.start()
444
445 - def run(self):
446 f = open(self.filename, self.mode) 447 f.write(self.content) 448 f.close() 449 os.unlink(self.filename) 450 if self.dirname is not None: 451 os.rmdir(self.dirname)
452 453
454 - class _FIFOFileItem(_FileItem):
455 """A _FileItem based on a FIFO (named pipe). 456 457 This class depends on the availablity of os.mkfifo(), which only 458 exists under Unix. 459 460 """ 461
462 - def __init__(self, content, **keyw):
463 # If the user hasn't specified a title, set it to None so that 464 # the name of the temporary FIFO is not used: 465 if 'title' not in keyw: 466 keyw['title'] = None 467 468 _FileItem.__init__(self, '', **keyw) 469 self.content = content 470 if keyw.get('binary', 0): 471 self.mode = 'wb' 472 else: 473 self.mode = 'w'
474
475 - def get_base_command_string(self):
476 """Create the gnuplot command for plotting this item. 477 478 The basecommand is different each time because each FIFOWriter 479 creates a new FIFO. 480 481 """ 482 483 # Create a new FIFO and a thread to write to it. Retrieve the 484 # filename of the FIFO to be used in the basecommand. 485 fifo = _FIFOWriter(self.content, self.mode) 486 return gp.double_quote_string(fifo.filename)
487 488
489 -def File(filename, **keyw):
490 """Construct a _FileItem object referring to an existing file. 491 492 This is a convenience function that just returns a _FileItem that 493 wraps the filename. 494 495 <filename> is a string holding the filename of an existing file. 496 The keyword arguments are the same as those of the _FileItem 497 constructor. 498 499 """ 500 501 if type(filename) is not types.StringType: 502 raise Errors.OptionError( 503 'Argument (%s) must be a filename' % (filename,) 504 ) 505 return _FileItem(filename, **keyw)
506 507
508 -def Data(*data, **keyw):
509 """Create and return a _FileItem representing the data from *data. 510 511 Create a '_FileItem' object (which is a type of 'PlotItem') out of 512 one or more Float Python numpy arrays (or objects that can be 513 converted to a float numpy array). If the routine is passed a 514 single with multiple dimensions, then the last index ranges over 515 the values comprising a single data point (e.g., [<x>, <y>, 516 <sigma>]) and the rest of the indices select the data point. If 517 passed a single array with 1 dimension, then each point is 518 considered to have only one value (i.e., by default the values 519 will be plotted against their indices). If the routine is passed 520 more than one array, they must have identical shapes, and then 521 each data point is composed of one point from each array. E.g., 522 'Data(x,x**2)' is a 'PlotItem' that represents x squared as a 523 function of x. For the output format, see the comments for 524 'write_array()'. 525 526 How the data are written to gnuplot depends on the 'inline' 527 argument and preference settings for the platform in use. 528 529 Keyword arguments: 530 531 'cols=<tuple>' -- write only the specified columns from each 532 data point to the file. Since cols is used by python, the 533 columns should be numbered in the python style (starting 534 from 0), not the gnuplot style (starting from 1). 535 536 'inline=<bool>' -- transmit the data to gnuplot 'inline' 537 rather than through a temporary file. The default is the 538 value of gp.GnuplotOpts.prefer_inline_data. 539 540 'filename=<string>' -- save data to a permanent file. 541 542 The keyword arguments recognized by '_FileItem' can also be used 543 here. 544 545 """ 546 547 if len(data) == 1: 548 # data was passed as a single structure 549 data = utils.float_array(data[0]) 550 551 # As a special case, if passed a single 1-D array, then it is 552 # treated as one value per point (by default, plotted against 553 # its index): 554 if len(data.shape) == 1: 555 data = data[:,numpy.newaxis] 556 else: 557 # data was passed column by column (for example, 558 # Data(x,y)); pack it into one big array (this will test 559 # that sizes are all the same): 560 data = utils.float_array(data) 561 dims = len(data.shape) 562 # transpose so that the last index selects x vs. y: 563 data = numpy.transpose(data, (dims-1,) + tuple(range(dims-1))) 564 if 'cols' in keyw: 565 cols = keyw['cols'] 566 del keyw['cols'] 567 if isinstance(cols, types.IntType): 568 cols = (cols,) 569 data = numpy.take(data, cols, -1) 570 571 if 'filename' in keyw: 572 filename = keyw['filename'] or None 573 del keyw['filename'] 574 else: 575 filename = None 576 577 if 'inline' in keyw: 578 inline = keyw['inline'] 579 del keyw['inline'] 580 if inline and filename: 581 raise Errors.OptionError( 582 'cannot pass data both inline and via a file' 583 ) 584 else: 585 inline = (not filename) and gp.GnuplotOpts.prefer_inline_data 586 587 # Output the content into a string: 588 f = StringIO() 589 utils.write_array(f, data) 590 content = f.getvalue() 591 if inline: 592 return _InlineFileItem(content, **keyw) 593 elif filename: 594 return _NewFileItem(content, filename=filename, **keyw) 595 elif gp.GnuplotOpts.prefer_fifo_data: 596 return _FIFOFileItem(content, **keyw) 597 else: 598 return _NewFileItem(content, **keyw)
599 600
601 -def GridData( 602 data, xvals=None, yvals=None, inline=_unset, filename=None, **keyw 603 ):
604 """Return a _FileItem representing a function of two variables. 605 606 'GridData' represents a function that has been tabulated on a 607 rectangular grid. The data are written to a file; no copy is kept 608 in memory. 609 610 Arguments: 611 612 'data' -- the data to plot: a 2-d array with dimensions 613 (numx,numy). 614 615 'xvals' -- a 1-d array with dimension 'numx' 616 617 'yvals' -- a 1-d array with dimension 'numy' 618 619 'binary=<bool>' -- send data to gnuplot in binary format? 620 621 'inline=<bool>' -- send data to gnuplot "inline"? 622 623 'filename=<string>' -- save data to a permanent file. 624 625 Note the unusual argument order! The data are specified *before* 626 the x and y values. (This inconsistency was probably a mistake; 627 after all, the default xvals and yvals are not very useful.) 628 629 'data' must be a data array holding the values of a function 630 f(x,y) tabulated on a grid of points, such that 'data[i,j] == 631 f(xvals[i], yvals[j])'. If 'xvals' and/or 'yvals' are omitted, 632 integers (starting with 0) are used for that coordinate. The data 633 are written to a temporary file; no copy of the data is kept in 634 memory. 635 636 If 'binary=0' then the data are written to a datafile as 'x y 637 f(x,y)' triplets (y changes most rapidly) that can be used by 638 gnuplot's 'splot' command. Blank lines are included each time the 639 value of x changes so that gnuplot knows to plot a surface through 640 the data. 641 642 If 'binary=1' then the data are written to a file in a binary 643 format that 'splot' can understand. Binary format is faster and 644 usually saves disk space but is not human-readable. If your 645 version of gnuplot doesn't support binary format (it is a 646 recently-added feature), this behavior can be disabled by setting 647 the configuration variable 648 'gp.GnuplotOpts.recognizes_binary_splot=0' in the appropriate 649 gp*.py file. 650 651 Thus if you have three arrays in the above format and a Gnuplot 652 instance called g, you can plot your data by typing 653 'g.splot(Gnuplot.GridData(data,xvals,yvals))'. 654 655 """ 656 657 # Try to interpret data as an array: 658 data = utils.float_array(data) 659 try: 660 (numx, numy) = data.shape 661 except ValueError: 662 raise Errors.DataError('data array must be two-dimensional') 663 664 if xvals is None: 665 xvals = numpy.arange(numx) 666 else: 667 xvals = utils.float_array(xvals) 668 if xvals.shape != (numx,): 669 raise Errors.DataError( 670 'The size of xvals must be the same as the size of ' 671 'the first dimension of the data array') 672 673 if yvals is None: 674 yvals = numpy.arange(numy) 675 else: 676 yvals = utils.float_array(yvals) 677 if yvals.shape != (numy,): 678 raise Errors.DataError( 679 'The size of yvals must be the same as the size of ' 680 'the second dimension of the data array') 681 682 # Binary defaults to true if recognizes_binary_plot is set; 683 # otherwise it is forced to false. 684 binary = keyw.get('binary', 1) and gp.GnuplotOpts.recognizes_binary_splot 685 keyw['binary'] = binary 686 687 if inline is _unset: 688 inline = ( 689 (not binary) and (not filename) 690 and gp.GnuplotOpts.prefer_inline_data 691 ) 692 elif inline and filename: 693 raise Errors.OptionError( 694 'cannot pass data both inline and via a file' 695 ) 696 697 # xvals, yvals, and data are now all filled with arrays of data. 698 if binary: 699 if inline: 700 raise Errors.OptionError('binary inline data not supported') 701 702 # write file in binary format 703 704 # It seems that the gnuplot documentation for binary mode 705 # disagrees with its actual behavior (as of v. 3.7). The 706 # documentation has the roles of x and y exchanged. We ignore 707 # the documentation and go with the code. 708 709 mout = numpy.zeros((numy + 1, numx + 1), numpy.float32) 710 mout[0,0] = numx 711 mout[0,1:] = xvals.astype(numpy.float32) 712 mout[1:,0] = yvals.astype(numpy.float32) 713 try: 714 # try copying without the additional copy implied by astype(): 715 mout[1:,1:] = numpy.transpose(data) 716 except: 717 # if that didn't work then downcasting from double 718 # must be necessary: 719 mout[1:,1:] = numpy.transpose(data.astype(numpy.float32)) 720 721 content = mout.tostring() 722 if (not filename) and gp.GnuplotOpts.prefer_fifo_data: 723 return _FIFOFileItem(content, **keyw) 724 else: 725 return _NewFileItem(content, filename=filename, **keyw) 726 else: 727 # output data to file as "x y f(x)" triplets. This 728 # requires numy copies of each x value and numx copies of 729 # each y value. First reformat the data: 730 set = numpy.transpose( 731 numpy.array( 732 (numpy.transpose(numpy.resize(xvals, (numy, numx))), 733 numpy.resize(yvals, (numx, numy)), 734 data)), (1,2,0)) 735 736 # Now output the data with the usual routine. This will 737 # produce data properly formatted in blocks separated by blank 738 # lines so that gnuplot can connect the points into a grid. 739 f = StringIO() 740 utils.write_array(f, set) 741 content = f.getvalue() 742 743 if inline: 744 return _InlineFileItem(content, **keyw) 745 elif filename: 746 return _NewFileItem(content, filename=filename, **keyw) 747 elif gp.GnuplotOpts.prefer_fifo_data: 748 return _FIFOFileItem(content, **keyw) 749 else: 750 return _NewFileItem(content, **keyw)
751