1
2
3
4
5
6
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
23
24
25
26
27 try:
28 import numpy
29 except ImportError:
30
31 import numpypy
32 import numpy
33
34 from . import gp, utils, Errors
35
36
38 """Used to represent unset keyword arguments."""
39
40 pass
41
42
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
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
102 _option_sequence = [
103 'binary',
104 'index', 'every', 'thru', 'using', 'smooth',
105 'axes', 'title', 'with'
106 ]
107
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
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
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
170
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
180 raise NotImplementedError()
181
189
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
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
239
242
243
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
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
316
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
344
345
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
357 self.temp = False
358 f = open(filename, mode)
359 else:
360 self.temp = True
361 if hasattr(tempfile, 'mkstemp'):
362
363 (fd, filename,) = tempfile.mkstemp(
364 suffix='.gnuplot', text=(not binary)
365 )
366 f = os.fdopen(fd, mode)
367 else:
368
369 filename = tempfile.mktemp()
370 f = open(filename, mode)
371
372 f.write(content)
373 f.close()
374
375
376
377 if self.temp and 'title' not in keyw:
378 keyw['title'] = None
379
380 _FileItem.__init__(self, filename, **keyw)
381
383 if self.temp:
384 os.unlink(self.filename)
385
386
388 """A _FileItem that actually indicates inline data.
389
390 """
391
393
394
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
409 f.write(self.content + 'e\n')
410
411
412 if gp.GnuplotOpts.support_fifo:
413 import threading
414
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
434 self.content = content
435 self.mode = mode
436 if hasattr(tempfile, 'mkdtemp'):
437
438
439 self.dirname = tempfile.mkdtemp(suffix='.gnuplot')
440 self.filename = os.path.join(self.dirname, 'fifo')
441 else:
442
443
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
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
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
471
472
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
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
492
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
557 data = utils.float_array(data[0])
558
559
560
561
562 if len(data.shape) == 1:
563 data = data[:,numpy.newaxis]
564 else:
565
566
567
568 data = utils.float_array(data)
569 dims = len(data.shape)
570
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
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
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
691
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
706 if binary:
707 if inline:
708 raise Errors.OptionError('binary inline data not supported')
709
710
711
712
713
714
715
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
723 mout[1:,1:] = numpy.transpose(data)
724 except:
725
726
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
736
737
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
745
746
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
761