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 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
34 """Used to represent unset keyword arguments."""
35
36 pass
37
38
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
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
98 _option_sequence = [
99 'binary',
100 'index', 'every', 'thru', 'using', 'smooth',
101 'axes', 'title', 'with'
102 ]
103
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
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
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
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
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
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
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
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
235
238
239
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
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
312
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
340
341
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
353 self.temp = False
354 f = open(filename, mode)
355 else:
356 self.temp = True
357 if hasattr(tempfile, 'mkstemp'):
358
359 (fd, filename,) = tempfile.mkstemp(
360 suffix='.gnuplot', text=(not binary)
361 )
362 f = os.fdopen(fd, mode)
363 else:
364
365 filename = tempfile.mktemp()
366 f = open(filename, mode)
367
368 f.write(content)
369 f.close()
370
371
372
373 if self.temp and 'title' not in keyw:
374 keyw['title'] = None
375
376 _FileItem.__init__(self, filename, **keyw)
377
379 if self.temp:
380 os.unlink(self.filename)
381
382
384 """A _FileItem that actually indicates inline data.
385
386 """
387
389
390
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
405 f.write(self.content + 'e\n')
406
407
408 if gp.GnuplotOpts.support_fifo:
409 import threading
410
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
430 self.content = content
431 self.mode = mode
432 if hasattr(tempfile, 'mkdtemp'):
433
434
435 self.dirname = tempfile.mkdtemp(suffix='.gnuplot')
436 self.filename = os.path.join(self.dirname, 'fifo')
437 else:
438
439
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
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
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
467
468
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
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
488
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
553 data = utils.float_array(data[0])
554
555
556
557
558 if len(data.shape) == 1:
559 data = data[:,numpy.newaxis]
560 else:
561
562
563
564 data = utils.float_array(data)
565 dims = len(data.shape)
566
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
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
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
687
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
702 if binary:
703 if inline:
704 raise Errors.OptionError('binary inline data not supported')
705
706
707
708
709
710
711
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
719 mout[1:,1:] = numpy.transpose(data)
720 except:
721
722
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
732
733
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
741
742
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