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