##// END OF EJS Templates
Implement object info protocol....
Implement object info protocol. Not fully finished yet, but most things work.

File last commit:

r2024:bb4e69c9
r2931:e7e389c1
Show More
Gnuplot2.py
665 lines | 25.9 KiB | text/x-python | PythonLexer
# -*- coding: utf-8 -*-
"""Improved replacement for the Gnuplot.Gnuplot class.
This module imports Gnuplot and replaces some of its functionality with
improved versions. They add better handling of arrays for plotting and more
convenient PostScript generation, plus some fixes for hardcopy().
It also adds a convenient plot2 method for plotting dictionaries and
lists/tuples of arrays.
This module is meant to be used as a drop-in replacement to the original
Gnuplot, so it should be safe to do:
import IPython.Gnuplot2 as Gnuplot
"""
import cStringIO
import os
import string
import sys
import tempfile
import time
import types
import Gnuplot as Gnuplot_ori
import Numeric
from IPython.utils.genutils import popkey,xsys
# needed by hardcopy():
gp = Gnuplot_ori.gp
# Patch for Gnuplot.py 1.6 compatibility.
# Thanks to Hayden Callow <h.callow@elec.canterbury.ac.nz>
try:
OptionException = Gnuplot_ori.PlotItems.OptionException
except AttributeError:
OptionException = Gnuplot_ori.Errors.OptionError
# exhibit a similar interface to Gnuplot so it can be somewhat drop-in
Data = Gnuplot_ori.Data
Func = Gnuplot_ori.Func
GridData = Gnuplot_ori.GridData
PlotItem = Gnuplot_ori.PlotItem
PlotItems = Gnuplot_ori.PlotItems
# Modify some of Gnuplot's functions with improved versions (or bugfixed, in
# hardcopy's case). In order to preserve the docstrings at runtime, I've
# copied them from the original code.
# After some significant changes in v 1.7 of Gnuplot.py, we need to do a bit
# of version checking.
if Gnuplot_ori.__version__ <= '1.6':
_BaseFileItem = PlotItems.File
_BaseTempFileItem = PlotItems.TempFile
# Fix the File class to add the 'index' option for Gnuplot versions < 1.7
class File(_BaseFileItem):
_option_list = _BaseFileItem._option_list.copy()
_option_list.update({
'index' : lambda self, index: self.set_option_index(index),
})
# A new initializer is needed b/c we want to add a modified
# _option_sequence list which includes 'index' in the right place.
def __init__(self,*args,**kw):
self._option_sequence = ['binary', 'index', 'using', 'smooth', 'axes',
'title', 'with']
_BaseFileItem.__init__(self,*args,**kw)
# Let's fix the constructor docstring
__newdoc = \
"""Additional Keyword arguments added by IPython:
'index=<int>' -- similar to the `index` keyword in Gnuplot.
This allows only some of the datasets in a file to be
plotted. Datasets within a file are assumed to be separated
by _pairs_ of blank lines, and the first one is numbered as
0 (similar to C/Python usage)."""
__init__.__doc__ = PlotItems.File.__init__.__doc__ + __newdoc
def set_option_index(self, index):
if index is None:
self.clear_option('index')
elif type(index) in [type(''), type(1)]:
self._options['index'] = (index, 'index %s' % index)
elif type(index) is type(()):
self._options['index'] = (index,'index %s' %
string.join(map(repr, index), ':'))
else:
raise OptionException('index=%s' % (index,))
# We need a FileClass with a different name from 'File', which is a
# factory function in 1.7, so that our String class can subclass FileClass
# in any version.
_FileClass = File
elif Gnuplot_ori.__version__ =='1.7':
_FileClass = _BaseFileItem = PlotItems._FileItem
_BaseTempFileItem = PlotItems._TempFileItem
File = PlotItems.File
else: # changes in the newer version (svn as of March'06)
_FileClass = _BaseFileItem = PlotItems._FileItem
_BaseTempFileItem = PlotItems._NewFileItem
File = PlotItems.File
# Now, we can add our generic code which is version independent
# First some useful utilities
def eps_fix_bbox(fname):
"""Fix the bounding box of an eps file by running ps2eps on it.
If its name ends in .eps, the original file is removed.
This is particularly useful for plots made by Gnuplot with square aspect
ratio: there is a bug in Gnuplot which makes it generate a bounding box
which is far wider than the actual plot.
This function assumes that ps2eps is installed in your system."""
# note: ps2ps and eps2eps do NOT work, ONLY ps2eps works correctly. The
# others make output with bitmapped fonts, which looks horrible.
print 'Fixing eps file: <%s>' % fname
xsys('ps2eps -f -q -l %s' % fname)
if fname.endswith('.eps'):
os.rename(fname+'.eps',fname)
def is_list1d(x,containers = [types.ListType,types.TupleType]):
"""Returns true if x appears to be a 1d list/tuple/array.
The heuristics are: identify Numeric arrays, or lists/tuples whose first
element is not itself a list/tuple. This way zipped lists should work like
the original Gnuplot. There's no inexpensive way to know if a list doesn't
have a composite object after its first element, so that kind of input
will produce an error. But it should work well in most cases.
"""
x_type = type(x)
return x_type == Numeric.ArrayType and len(x.shape)==1 or \
(x_type in containers and
type(x[0]) not in containers + [Numeric.ArrayType])
def zip_items(items,titles=None):
"""zip together neighboring 1-d arrays, and zip standalone ones
with their index. Leave other plot items alone."""
class StandaloneItem(Exception): pass
def get_titles(titles):
"""Return the next title and the input titles array.
The input array may be changed to None when no titles are left to
prevent extra unnecessary calls to this function."""
try:
title = titles[tit_ct[0]] # tit_ct[0] is in zip_items'scope
except IndexError:
titles = None # so we don't enter again
title = None
else:
tit_ct[0] += 1
return title,titles
new_items = []
if titles:
# Initialize counter. It was put in a list as a hack to allow the
# nested get_titles to modify it without raising a NameError.
tit_ct = [0]
n = 0 # this loop needs to be done by hand
while n < len(items):
item = items[n]
try:
if is_list1d(item):
if n==len(items)-1: # last in list
raise StandaloneItem
else: # check the next item and zip together if needed
next_item = items[n+1]
if next_item is None:
n += 1
raise StandaloneItem
elif is_list1d(next_item):
# this would be best done with an iterator
if titles:
title,titles = get_titles(titles)
else:
title = None
new_items.append(Data(zip(item,next_item),
title=title))
n += 1 # avoid double-inclusion of next item
else: # can't zip with next, zip with own index list
raise StandaloneItem
else: # not 1-d array
new_items.append(item)
except StandaloneItem:
if titles:
title,titles = get_titles(titles)
else:
title = None
new_items.append(Data(zip(range(len(item)),item),title=title))
except AttributeError:
new_items.append(item)
n+=1
return new_items
# And some classes with enhanced functionality.
class String(_FileClass):
"""Make a PlotItem from data in a string with the same format as a File.
This allows writing data directly inside python scripts using the exact
same format and manipulation options which would be used for external
files."""
def __init__(self, data_str, **keyw):
"""Construct a String object.
<data_str> is a string formatted exactly like a valid Gnuplot data
file would be. All options from the File constructor are valid here.
Warning: when used for interactive plotting in scripts which exit
immediately, you may get an error because the temporary file used to
hold the string data was deleted before Gnuplot had a chance to see
it. You can work around this problem by putting a raw_input() call at
the end of the script.
This problem does not appear when generating PostScript output, only
with Gnuplot windows."""
self.tmpfile = _BaseTempFileItem()
tmpfile = file(self.tmpfile.filename,'w')
tmpfile.write(data_str)
_BaseFileItem.__init__(self,self.tmpfile,**keyw)
class Gnuplot(Gnuplot_ori.Gnuplot):
"""Improved Gnuplot class.
Enhancements: better plot,replot and hardcopy methods. New methods for
quick range setting.
"""
def xrange(self,min='*',max='*'):
"""Set xrange. If min/max is omitted, it is set to '*' (auto).
Note that this is different from the regular Gnuplot behavior, where
an unspecified limit means no change. Here any unspecified limit is
set to autoscaling, allowing these functions to be used for full
autoscaling when called with no arguments.
To preserve one limit's current value while changing the other, an
explicit '' argument must be given as the limit to be kept.
Similar functions exist for [y{2}z{2}rtuv]range."""
self('set xrange [%s:%s]' % (min,max))
def yrange(self,min='*',max='*'):
self('set yrange [%s:%s]' % (min,max))
def zrange(self,min='*',max='*'):
self('set zrange [%s:%s]' % (min,max))
def x2range(self,min='*',max='*'):
self('set xrange [%s:%s]' % (min,max))
def y2range(self,min='*',max='*'):
self('set yrange [%s:%s]' % (min,max))
def z2range(self,min='*',max='*'):
self('set zrange [%s:%s]' % (min,max))
def rrange(self,min='*',max='*'):
self('set rrange [%s:%s]' % (min,max))
def trange(self,min='*',max='*'):
self('set trange [%s:%s]' % (min,max))
def urange(self,min='*',max='*'):
self('set urange [%s:%s]' % (min,max))
def vrange(self,min='*',max='*'):
self('set vrange [%s:%s]' % (min,max))
def set_ps(self,option):
"""Set an option for the PostScript terminal and reset default term."""
self('set terminal postscript %s ' % option)
self('set terminal %s' % gp.GnuplotOpts.default_term)
def __plot_ps(self, plot_method,*items, **keyw):
"""Wrapper for plot/splot/replot, with processing of hardcopy options.
For internal use only."""
# Filter out PostScript options which will crash the normal plot/replot
psargs = {'filename':None,
'mode':None,
'eps':None,
'enhanced':None,
'color':None,
'solid':None,
'duplexing':None,
'fontname':None,
'fontsize':None,
'debug':0 }
for k in psargs.keys():
if keyw.has_key(k):
psargs[k] = keyw[k]
del keyw[k]
# Filter out other options the original plot doesn't know
hardcopy = popkey(keyw,'hardcopy',psargs['filename'] is not None)
titles = popkey(keyw,'titles',0)
# the filename keyword should control hardcopy generation, this is an
# override switch only which needs to be explicitly set to zero
if hardcopy:
if psargs['filename'] is None:
raise ValueError, \
'If you request hardcopy, you must give a filename.'
# set null output so nothing goes to screen. hardcopy() restores output
self('set term dumb')
# I don't know how to prevent screen output in Windows
if os.name == 'posix':
self('set output "/dev/null"')
new_items = zip_items(items,titles)
# plot_method is either plot or replot from the original Gnuplot class:
plot_method(self,*new_items,**keyw)
# Do hardcopy if requested
if hardcopy:
if psargs['filename'].endswith('.eps'):
psargs['eps'] = 1
self.hardcopy(**psargs)
def plot(self, *items, **keyw):
"""Draw a new plot.
Clear the current plot and create a new 2-d plot containing
the specified items. Each arguments should be of the
following types:
'PlotItem' (e.g., 'Data', 'File', 'Func') -- This is the most
flexible way to call plot because the PlotItems can
contain suboptions. Moreover, PlotItems can be saved to
variables so that their lifetime is longer than one plot
command; thus they can be replotted with minimal overhead.
'string' (e.g., 'sin(x)') -- The string is interpreted as
'Func(string)' (a function that is computed by gnuplot).
Anything else -- The object, which should be convertible to an
array, is passed to the 'Data' constructor, and thus
plotted as data. If the conversion fails, an exception is
raised.
This is a modified version of plot(). Compared to the original in
Gnuplot.py, this version has several enhancements, listed below.
Modifications to the input arguments
------------------------------------
(1-d array means Numeric array, list or tuple):
(i) Any 1-d array which is NOT followed by another 1-d array, is
automatically zipped with range(len(array_1d)). Typing g.plot(y) will
plot y against its indices.
(ii) If two 1-d arrays are contiguous in the argument list, they are
automatically zipped together. So g.plot(x,y) plots y vs. x, and
g.plot(x1,y1,x2,y2) plots y1 vs. x1 and y2 vs. x2.
(iii) Any 1-d array which is followed by None is automatically zipped
with range(len(array_1d)). In this form, typing g.plot(y1,None,y2)
will plot both y1 and y2 against their respective indices (and NOT
versus one another). The None prevents zipping y1 and y2 together, and
since y2 is unpaired it is automatically zipped to its indices by (i)
(iv) Any other arguments which don't match these cases are left alone and
passed to the code below.
For lists or tuples, the heuristics used to determine whether they are
in fact 1-d is fairly simplistic: their first element is checked, and
if it is not a list or tuple itself, it is assumed that the whole
object is one-dimensional.
An additional optional keyword 'titles' has been added: it must be a
list of strings to be used as labels for the individual plots which
are NOT PlotItem objects (since those objects carry their own labels
within).
PostScript generation
---------------------
This version of plot() also handles automatically the production of
PostScript output. The main options are (given as keyword arguments):
- filename: a string, typically ending in .eps. If given, the plot is
sent to this file in PostScript format.
- hardcopy: this can be set to 0 to override 'filename'. It does not
need to be given to produce PostScript, its purpose is to allow
switching PostScript output off globally in scripts without having to
manually change 'filename' values in multiple calls.
All other keywords accepted by Gnuplot.hardcopy() are transparently
passed, and safely ignored if output is sent to the screen instead of
PostScript.
For example:
In [1]: x=frange(0,2*pi,npts=100)
Generate a plot in file 'sin.eps':
In [2]: plot(x,sin(x),filename = 'sin.eps')
Plot to screen instead, without having to change the filename:
In [3]: plot(x,sin(x),filename = 'sin.eps',hardcopy=0)
Pass the 'color=0' option to hardcopy for monochrome output:
In [4]: plot(x,sin(x),filename = 'sin.eps',color=0)
PostScript generation through plot() is useful mainly for scripting
uses where you are not interested in interactive plotting. For
interactive use, the hardcopy() function is typically more convenient:
In [5]: plot(x,sin(x))
In [6]: hardcopy('sin.eps') """
self.__plot_ps(Gnuplot_ori.Gnuplot.plot,*items,**keyw)
def plot2(self,arg,**kw):
"""Plot the entries of a dictionary or a list/tuple of arrays.
This simple utility calls plot() with a list of Gnuplot.Data objects
constructed either from the values of the input dictionary, or the entries
in it if it is a tuple or list. Each item gets labeled with the key/index
in the Gnuplot legend.
Each item is plotted by zipping it with a list of its indices.
Any keywords are passed directly to plot()."""
if hasattr(arg,'keys'):
keys = arg.keys()
keys.sort()
else:
keys = range(len(arg))
pitems = [Data(zip(range(len(arg[k])),arg[k]),title=`k`) for k in keys]
self.plot(*pitems,**kw)
def splot(self, *items, **keyw):
"""Draw a new three-dimensional plot.
Clear the current plot and create a new 3-d plot containing
the specified items. Arguments can be of the following types:
'PlotItem' (e.g., 'Data', 'File', 'Func', 'GridData' ) -- This
is the most flexible way to call plot because the
PlotItems can contain suboptions. Moreover, PlotItems can
be saved to variables so that their lifetime is longer
than one plot command--thus they can be replotted with
minimal overhead.
'string' (e.g., 'sin(x*y)') -- The string is interpreted as a
'Func()' (a function that is computed by gnuplot).
Anything else -- The object is converted to a Data() item, and
thus plotted as data. Note that each data point should
normally have at least three values associated with it
(i.e., x, y, and z). If the conversion fails, an
exception is raised.
This is a modified version of splot(). Compared to the original in
Gnuplot.py, this version has several enhancements, listed in the
plot() documentation.
"""
self.__plot_ps(Gnuplot_ori.Gnuplot.splot,*items,**keyw)
def replot(self, *items, **keyw):
"""Replot the data, possibly adding new 'PlotItem's.
Replot the existing graph, using the items in the current
itemlist. If arguments are specified, they are interpreted as
additional items to be plotted alongside the existing items on
the same graph. See 'plot' for details.
If you want to replot to a postscript file, you MUST give the
'filename' keyword argument in each call to replot. The Gnuplot python
interface has no way of knowing that your previous call to
Gnuplot.plot() was meant for PostScript output."""
self.__plot_ps(Gnuplot_ori.Gnuplot.replot,*items,**keyw)
# The original hardcopy has a bug. See fix at the end. The rest of the code
# was lifted verbatim from the original, so that people using IPython get the
# benefits without having to manually patch Gnuplot.py
def hardcopy(self, filename=None,
mode=None,
eps=None,
enhanced=None,
color=None,
solid=None,
duplexing=None,
fontname=None,
fontsize=None,
debug = 0,
):
"""Create a hardcopy of the current plot.
Create a postscript hardcopy of the current plot to the
default printer (if configured) or to the specified filename.
Note that gnuplot remembers the postscript suboptions across
terminal changes. Therefore if you set, for example, color=1
for one hardcopy then the next hardcopy will also be color
unless you explicitly choose color=0. Alternately you can
force all of the options to their defaults by setting
mode='default'. I consider this to be a bug in gnuplot.
Keyword arguments:
'filename=<string>' -- if a filename is specified, save the
output in that file; otherwise print it immediately
using the 'default_lpr' configuration option. If the
filename ends in '.eps', EPS mode is automatically
selected (like manually specifying eps=1 or mode='eps').
'mode=<string>' -- set the postscript submode ('landscape',
'portrait', 'eps', or 'default'). The default is
to leave this option unspecified.
'eps=<bool>' -- shorthand for 'mode="eps"'; asks gnuplot to
generate encapsulated postscript.
'enhanced=<bool>' -- if set (the default), then generate
enhanced postscript, which allows extra features like
font-switching, superscripts, and subscripts in axis
labels. (Some old gnuplot versions do not support
enhanced postscript; if this is the case set
gp.GnuplotOpts.prefer_enhanced_postscript=None.)
'color=<bool>' -- if set, create a plot with color. Default
is to leave this option unchanged.
'solid=<bool>' -- if set, force lines to be solid (i.e., not
dashed).
'duplexing=<string>' -- set duplexing option ('defaultplex',
'simplex', or 'duplex'). Only request double-sided
printing if your printer can handle it. Actually this
option is probably meaningless since hardcopy() can only
print a single plot at a time.
'fontname=<string>' -- set the default font to <string>,
which must be a valid postscript font. The default is
to leave this option unspecified.
'fontsize=<double>' -- set the default font size, in
postscript points.
'debug=<bool>' -- print extra debugging information (useful if
your PostScript files are misteriously not being created).
"""
if filename is None:
assert gp.GnuplotOpts.default_lpr is not None, \
OptionException('default_lpr is not set, so you can only '
'print to a file.')
filename = gp.GnuplotOpts.default_lpr
lpr_output = 1
else:
if filename.endswith('.eps'):
eps = 1
lpr_output = 0
# Be careful processing the options. If the user didn't
# request an option explicitly, do not specify it on the 'set
# terminal' line (don't even specify the default value for the
# option). This is to avoid confusing older versions of
# gnuplot that do not support all of these options. The
# exception is 'enhanced', which is just too useful to have to
# specify each time!
setterm = ['set', 'terminal', 'postscript']
if eps:
assert mode is None or mode=='eps', \
OptionException('eps option and mode are incompatible')
setterm.append('eps')
else:
if mode is not None:
assert mode in ['landscape', 'portrait', 'eps', 'default'], \
OptionException('illegal mode "%s"' % mode)
setterm.append(mode)
if enhanced is None:
enhanced = gp.GnuplotOpts.prefer_enhanced_postscript
if enhanced is not None:
if enhanced: setterm.append('enhanced')
else: setterm.append('noenhanced')
if color is not None:
if color: setterm.append('color')
else: setterm.append('monochrome')
if solid is not None:
if solid: setterm.append('solid')
else: setterm.append('dashed')
if duplexing is not None:
assert duplexing in ['defaultplex', 'simplex', 'duplex'], \
OptionException('illegal duplexing mode "%s"' % duplexing)
setterm.append(duplexing)
if fontname is not None:
setterm.append('"%s"' % fontname)
if fontsize is not None:
setterm.append('%s' % fontsize)
self(string.join(setterm))
self.set_string('output', filename)
# replot the current figure (to the printer):
self.refresh()
# fperez. Ugly kludge: often for some reason the file is NOT created
# and we must reissue the creation commands. I have no idea why!
if not lpr_output:
#print 'Hardcopy <%s>' % filename # dbg
maxtries = 20
delay = 0.1 # delay (in seconds) between print attempts
for i in range(maxtries):
time.sleep(0.05) # safety, very small delay
if os.path.isfile(filename):
if debug:
print 'Hardcopy to file <%s> success at attempt #%s.' \
% (filename,i+1)
break
time.sleep(delay)
# try again, issue all commands just in case
self(string.join(setterm))
self.set_string('output', filename)
self.refresh()
if not os.path.isfile(filename):
print >> sys.stderr,'ERROR: Tried %s times and failed to '\
'create hardcopy file `%s`' % (maxtries,filename)
# reset the terminal to its `default' setting:
self('set terminal %s' % gp.GnuplotOpts.default_term)
self.set_string('output')
#********************** End of file <Gnuplot2.py> ************************