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 import numpy
25
26 import gp, utils, Errors
27
28
30 """Used to represent unset keyword arguments."""
31
32 pass
33
34
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
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
94 _option_sequence = [
95 'binary',
96 'index', 'every', 'thru', 'using', 'smooth',
97 'axes', 'title', 'with'
98 ]
99
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
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
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
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
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
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
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
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
231
234
235
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
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
308
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
336
337
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
349 self.temp = False
350 f = open(filename, mode)
351 else:
352 self.temp = True
353 if hasattr(tempfile, 'mkstemp'):
354
355 (fd, filename,) = tempfile.mkstemp(
356 suffix='.gnuplot', text=(not binary)
357 )
358 f = os.fdopen(fd, mode)
359 else:
360
361 filename = tempfile.mktemp()
362 f = open(filename, mode)
363
364 f.write(content)
365 f.close()
366
367
368
369 if self.temp and 'title' not in keyw:
370 keyw['title'] = None
371
372 _FileItem.__init__(self, filename, **keyw)
373
375 if self.temp:
376 os.unlink(self.filename)
377
378
380 """A _FileItem that actually indicates inline data.
381
382 """
383
385
386
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
401 f.write(self.content + 'e\n')
402
403
404 if gp.GnuplotOpts.support_fifo:
405 import threading
406
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
426 self.content = content
427 self.mode = mode
428 if hasattr(tempfile, 'mkdtemp'):
429
430
431 self.dirname = tempfile.mkdtemp(suffix='.gnuplot')
432 self.filename = os.path.join(self.dirname, 'fifo')
433 else:
434
435
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
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
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
463
464
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
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
484
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
549 data = utils.float_array(data[0])
550
551
552
553
554 if len(data.shape) == 1:
555 data = data[:,numpy.newaxis]
556 else:
557
558
559
560 data = utils.float_array(data)
561 dims = len(data.shape)
562
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
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
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
683
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
698 if binary:
699 if inline:
700 raise Errors.OptionError('binary inline data not supported')
701
702
703
704
705
706
707
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
715 mout[1:,1:] = numpy.transpose(data)
716 except:
717
718
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
728
729
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
737
738
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