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