2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 import re
, ConfigParser
27 from pyx
import mathtree
, text
28 from pyx
.graph
import style
33 # fallback implementation for Python 2.2. and below
35 return zip(xrange(len(list)), list)
38 """Interface for graph data
40 Graph data consists in columns, where each column might
41 be identified by a string or an integer. Each row in the
42 resulting table refers to a data point.
44 All methods except for the constructor should consider
45 self to be readonly, since the data instance might be shared
46 between several graphs simultaniously. The plotitem instance
47 created by the graph is available as a container class."""
49 def getcolumnpointsindex(self
, column
):
52 This method returns data of a column by a tuple data, index.
53 column identifies the column. If index is not None, the data
54 of the column is found at position index for each element of
55 the list data. If index is None, the data is the list of
58 Some data might not be available by this function since it
59 is dynamic, i.e. it depends on plotitem. An example are
60 function data, which is available within a graph only. Thus
61 this method might raise an exception."""
62 raise NotImplementedError("call to an abstract method of %r" % self
)
64 def getcolumn(self
, column
):
67 This method returns the data of a column in a list. column
68 has the same meaning as in getcolumnpointsindex. Note, that
69 this method typically has to create a new list, which needs
70 time and memory. While its easy to the user, internally
71 it should be avoided in favor of getcolumnpointsindex."""
72 raise NotImplementedError("call to an abstract method of %r" % self
)
77 This method returns the number of points. All results by
78 getcolumnpointsindex and getcolumn will fit this number.
79 It might raise an exception as getcolumnpointsindex."""
80 raise NotImplementedError("call to an abstract method of %r" % self
)
82 def getdefaultstyles(self
):
83 """Default styles for the data
85 Returns a list of default styles for the data. Note to
86 return the same instances when the graph should iterate
87 over the styles using selectstyles."""
88 raise NotImplementedError("call to an abstract method of %r" % self
)
93 This method returns a title string for the data to be used
94 in graph keys and probably other locations. The method might
95 return None to indicate, that there is no title and the data
96 should be skiped in a graph key. Data titles does not need
98 raise NotImplementedError("call to an abstract method of %r" % self
)
100 def initplotitem(self
, plotitem
, graph
):
101 """Initialize plotitem
103 This function is called within the plotitem initialization
104 procedure and allows to initialize the plotitem as a data
105 container. For static data the method might just do nothing."""
106 raise NotImplementedError("call to an abstract method of %r" % self
)
108 def getcolumnpointsindex_plotitem(self
, plotitem
, column
):
109 """Data for a column with plotitem
111 Like getcolumnpointsindex but for use within a graph, i.e. with
112 a plotitem container class. For static data being defined within
113 the constructor already, the plotitem reference is not needed and
114 the method can be implemented by calling getcolumnpointsindex."""
115 raise NotImplementedError("call to an abstract method of %r" % self
)
117 def getcolumnnames(self
, plotitem
):
118 """Return list of column names of the data
120 This method returns a list of column names. It might
121 depend on the graph. (*YES*, it might depend on the graph
122 in case of a function, where function variables might be
123 axis names. Other variables, not available by axes, will
124 be taken from the context.)"""
125 raise NotImplementedError("call to an abstract method of %r" % self
)
127 def adjustaxes(self
, plotitem
, graph
, step
):
128 """Adjust axes ranges
130 This method should call adjustaxis for all styles.
131 On step == 0 axes with fixed data should be adjusted.
132 On step == 1 the current axes ranges might be used to
133 calculate further data (e.g. y data for a function y=f(x)
134 where the y range depends on the x range). On step == 2
135 axes ranges not previously set should be updated by data
136 accumulated by step 1."""
137 raise NotImplementedError("call to an abstract method of %r" % self
)
139 def draw(self
, plotitem
, graph
):
142 This method should draw the data. Its called by plotinfo,
143 since it can be implemented here more efficiently by avoiding
144 some additional function calls."""
145 raise NotImplementedError("call an abstract method of %r" % self
)
149 """Partly implements the _Idata interface"""
151 defaultstyles
= [style
.symbol()]
153 def getcolumn(self
, column
):
154 data
, index
= self
.getcolumnpointsindex(column
)
158 return [point
[index
] for point
in data
]
160 def getdefaultstyles(self
):
161 return self
.defaultstyles
166 def initplotitem(self
, plotitem
, graph
):
169 def draw(self
, plotitem
, graph
):
170 columnpointsindex
= []
172 for column
in self
.getcolumnnames(plotitem
):
173 points
, index
= self
.getcolumnpointsindex_plotitem(plotitem
, column
)
174 columnpointsindex
.append((column
, points
, index
))
179 raise ValueError("points len differs")
180 for privatedata
, style
in zip(plotitem
.privatedatalist
, plotitem
.styles
):
181 style
.initdrawpoints(privatedata
, plotitem
.sharedata
, graph
)
182 if len(columnpointsindex
):
183 plotitem
.sharedata
.point
= {}
185 for column
, points
, index
in columnpointsindex
:
186 if index
is not None:
187 plotitem
.sharedata
.point
[column
] = points
[i
][index
]
189 plotitem
.sharedata
.point
[column
] = points
[i
]
190 for privatedata
, style
in zip(plotitem
.privatedatalist
, plotitem
.styles
):
191 style
.drawpoint(privatedata
, plotitem
.sharedata
, graph
)
192 for privatedata
, style
in zip(plotitem
.privatedatalist
, plotitem
.styles
):
193 style
.donedrawpoints(privatedata
, plotitem
.sharedata
, graph
)
196 class _staticdata(_data
):
197 """Partly implements the _Idata interface
199 This class partly implements the _Idata interface for static data
200 using self.columns and self.points to be initialized by the constructor."""
202 def getcolumnpointsindex(self
, column
):
203 return self
.points
, self
.columns
[column
]
206 return len(self
.points
)
208 def getcolumnnames(self
, plotitem
):
209 return self
.columns
.keys()
211 def getcolumnpointsindex_plotitem(self
, plotitem
, column
):
212 return self
.getcolumnpointsindex(column
)
214 def adjustaxes(self
, plotitem
, graph
, step
):
216 for column
in self
.getcolumnnames(plotitem
):
217 points
, index
= self
.getcolumnpointsindex_plotitem(plotitem
, column
)
218 for privatedata
, style
in zip(plotitem
.privatedatalist
, plotitem
.styles
):
219 style
.adjustaxis(privatedata
, plotitem
.sharedata
, graph
, column
, points
, index
)
222 class _dynamicdata(_data
):
224 def getcolumnpointsindex(self
, column
):
225 raise RuntimeError("dynamic data not available outside a graph")
228 raise RuntimeError("dynamic data typically has no fixed number of points")
231 class list(_staticdata
):
232 "Graph data from a list of points"
234 def getcolumnpointsindex(self
, column
):
236 if self
.addlinenumbers
:
237 index
= self
.columns
[column
]-1
239 index
= self
.columns
[column
]
242 if type(column
) != type(column
+ 0):
243 raise ValueError("integer expected")
245 raise ValueError("integer expected")
246 if self
.addlinenumbers
:
252 return range(1, 1+len(self
.points
)), None
255 return self
.points
, index
257 def __init__(self
, points
, title
="user provided list", addlinenumbers
=1, **columns
):
259 # be paranoid and check each row to have the same number of points
263 raise ValueError("different number of columns per point")
264 for v
in columns
.values():
265 if abs(v
) > l
or (not addlinenumbers
and abs(v
) == l
):
266 raise ValueError("column number bigger than number of columns")
268 self
.columns
= columns
270 self
.addlinenumbers
= addlinenumbers
273 ##############################################################
274 # math tree enhanced by column number variables
275 ##############################################################
277 class MathTreeFuncCol(mathtree
.MathTreeFunc1
):
279 def __init__(self
, *args
):
280 mathtree
.MathTreeFunc1
.__init__(self
, "_column_", *args
)
283 # we misuse VarList here:
284 # - instead of returning a string, we return this instance itself
285 # - before calculating the expression, you must call ColumnNameAndNumber
286 # once (when limiting the context to external defined variables,
287 # otherwise you have to call it each time)
290 def ColumnNameAndNumber(_hidden_self
, **args
):
291 number
= int(_hidden_self
.Args
[0].Calc(**args
))
292 _hidden_self
.varname
= "_column_%i" % number
293 return _hidden_self
.varname
, number
298 def Calc(_hidden_self
, **args
):
299 return args
[_hidden_self
.varname
]
301 MathTreeFuncsWithCol
= mathtree
.DefaultMathTreeFuncs
+ [MathTreeFuncCol
]
306 def __init__(self
, tree
):
308 self
.Calc
= tree
.Calc
309 self
.__str
__ = tree
.__str
__
312 # returns a list of regular variables (strings) like the original mathtree
313 return [var
for var
in self
.tree
.VarList() if not isinstance(var
, MathTreeFuncCol
) and var
[:8] != "_column_"]
315 def columndict(_hidden_self
, **context
):
316 # returns a dictionary of column names (keys) and column numbers (values)
318 for var
in _hidden_self
.tree
.VarList():
319 if isinstance(var
, MathTreeFuncCol
):
320 name
, number
= var
.ColumnNameAndNumber(**context
)
321 columndict
[name
] = number
322 elif var
[:8] == "_column_":
323 columndict
[var
] = int(var
[8:])
327 class dataparser(mathtree
.parser
):
328 # mathtree parser enhanced by column handling
329 # parse returns a columntree instead of a regular tree
331 def __init__(self
, MathTreeFuncs
=MathTreeFuncsWithCol
, **kwargs
):
332 mathtree
.parser
.__init
__(self
, MathTreeFuncs
=MathTreeFuncs
, **kwargs
)
334 def parse(self
, expr
):
335 return columntree(mathtree
.parser
.parse(self
, expr
.replace("$", "_column_")))
337 ##############################################################
341 """this is a helper class to mark, that no title was privided
342 (since a title equals None is a valid input, it needs to be
343 distinguished from providing no title when a title will be
344 created automatically)"""
347 class data(_staticdata
):
348 "creates a new data set out of an existing data set"
350 def __init__(self
, data
, title
=notitle
, parser
=dataparser(), context
={}, **columns
):
353 items
= columns
.items()
354 items
.sort() # we want sorted items (otherwise they would be unpredictable scrambled)
355 self
.title
= "%s: %s" % (data
.title
,
356 ", ".join(["%s=%s" % (text
.escapestring(key
),
357 text
.escapestring(value
))
358 for key
, value
in items
]))
364 # analyse the **columns argument
367 for column
, value
in columns
.items():
369 # try if it is a valid column identifier
370 self
.columns
[column
] = self
.orgdata
.getcolumnpointsindex(value
)
371 except (KeyError, ValueError):
372 # take it as a mathematical expression
373 tree
= parser
.parse(value
)
374 columndict
= tree
.columndict(**context
)
376 for var
, value
in columndict
.items():
377 # column data accessed via $<column number>
378 points
, index
= self
.orgdata
.getcolumnpointsindex(value
)
379 varpointsindex
.append((var
, points
, index
))
380 for var
in tree
.VarList():
382 # column data accessed via the name of the column
383 points
, index
= self
.orgdata
.getcolumnpointsindex(var
)
384 except (KeyError, ValueError):
385 # other data available in context
386 if var
not in context
.keys():
387 raise ValueError("undefined variable '%s'" % var
)
389 varpointsindex
.append((var
, points
, index
))
390 newdata
= [None]*self
.getcount()
391 vars = context
.copy() # do not modify context, use a copy vars instead
392 for i
in xrange(self
.getcount()):
393 # insert column data as prepared in varpointsindex
394 for var
, point
, index
in varpointsindex
:
395 if index
is not None:
396 vars[var
] = points
[i
][index
]
398 vars[var
] = points
[i
]
399 # evaluate expression
401 newdata
[i
] = tree
.Calc(**vars)
402 except (ArithmeticError, ValueError):
405 # point[newcolumnnumber] = eval(str(tree), vars)
407 # XXX: It might happen, that the evaluation of the expression
408 # seems to work, but the result is NaN/Inf/-Inf. This
409 # is highly plattform dependend.
411 self
.columns
[column
] = newdata
, None
413 def getcolumnpointsindex(self
, column
):
414 return self
.columns
[column
]
417 return self
.orgdata
.getcount()
419 def getdefaultstyles(self
):
420 return self
.orgdata
.getdefaultstyles()
427 defaultcommentpattern
= re
.compile(r
"(#+|!+|%+)\s*")
428 defaultstringpattern
= re
.compile(r
"\"(.*?
)\"(\s
+|$
)")
429 defaultcolumnpattern = re.compile(r"(.*?
)(\s
+|$
)")
431 def splitline(self, line, stringpattern, columnpattern, tofloat=1):
432 """returns a tuple created out of the string line
433 - matches stringpattern and columnpattern, adds the first group of that
434 match to the result and and removes those matches until the line is empty
435 - when stringpattern matched, the result is always kept as a string
436 - when columnpattern matched and tofloat is true, a conversion to a float
437 is tried; when this conversion fails, the string is kept"""
439 # try to gain speed by skip matching regular expressions
440 if line.find('"')!=-1 or \
441 stringpattern is not self.defaultstringpattern or \
442 columnpattern is not self.defaultcolumnpattern:
444 match = stringpattern.match(line)
446 result.append(match.groups()[0])
447 line = line[match.end():]
449 match = columnpattern.match(line)
452 result.append(float(match.groups()[0]))
453 except (TypeError, ValueError):
454 result.append(match.groups()[0])
456 result.append(match.groups()[0])
457 line = line[match.end():]
461 return map(float, line.split())
462 except (TypeError, ValueError):
464 for r in line.split():
466 result.append(float(r))
467 except (TypeError, ValueError):
473 def getcachekey(self, *args):
474 return ":".join([str(x) for x in args])
476 def __init__(self, filename,
477 commentpattern=defaultcommentpattern,
478 stringpattern=defaultstringpattern,
479 columnpattern=defaultcolumnpattern,
480 skiphead=0, skiptail=0, every=1,
483 def readfile(file, title):
488 for line in file.readlines():
490 match = commentpattern.match(line)
493 keys = self.splitline(line[match.end():], stringpattern, columnpattern, tofloat=0)
496 i += 1 # the first column is number 1 since a linenumber is added in front
500 for value in self.splitline(line, stringpattern, columnpattern, tofloat=1):
501 linedata.append(value)
503 if linenumber >= skiphead and not ((linenumber - skiphead) % every):
504 linedata = [linenumber + 1] + linedata
505 if len(linedata) > maxcolumns:
506 maxcolumns = len(linedata)
507 points.append(linedata)
509 if skiptail >= every:
510 skip, x = divmod(skiptail, every)
512 for i in xrange(len(points)):
513 if len(points[i]) != maxcolumns:
514 points[i].extend([None]*(maxcolumns-len(points[i])))
515 return list(points, title=title, addlinenumbers=0, **columns)
520 # not a file-like object -> open it
521 cachekey = self.getcachekey(filename, commentpattern, stringpattern, columnpattern, skiphead, skiptail, every)
522 if not filecache.has_key(cachekey):
523 filecache[cachekey] = readfile(open(filename), filename)
524 data.__init__(self, filecache[cachekey], **kwargs)
526 data.__init__(self, readfile(filename, "user provided file-like object"), **kwargs)
531 class conffile(data):
533 def __init__(self, filename, **kwargs):
534 """read data from a config-like file
535 - filename is a string
536 - each row is defined by a section in the config-like file (see
537 config module description)
538 - the columns for each row are defined by lines in the section file;
539 the option entries identify and name the columns
540 - further keyword arguments are passed to the constructor of data,
541 keyword arguments data and titles excluded"""
543 def readfile(file, title):
544 config = ConfigParser.ConfigParser()
545 config.optionxform = str
547 sections = config.sections()
549 points = [None]*len(sections)
552 for i in xrange(len(sections)):
553 point = [sections[i]] + [None]*(maxcolumns-1)
554 for option in config.options(sections[i]):
555 value = config.get(sections[i], option)
561 index = columns[option]
563 columns[option] = maxcolumns
569 return list(points, title=title, addlinenumbers=0, **columns)
574 # not a file-like object -> open it
575 if not filecache.has_key(filename):
576 filecache[filename] = readfile(open(filename), filename)
577 data.__init__(self, filecache[filename], **kwargs)
579 data.__init__(self, readfile(filename, "user provided file-like object"), **kwargs)
582 class function(_dynamicdata):
584 defaultstyles = [style.line()]
586 def __init__(self, expression, title=notitle, min=None, max=None,
587 points=100, parser=mathtree.parser(), context={}):
590 self.title = expression
595 self.numberofpoints = points
596 self.context = context.copy() # be save on late evaluations
597 self.yname, expression = [x.strip() for x in expression.split("=")]
598 self.mathtree = parser.parse(expression)
600 def getcolumnpointsindex_plotitem(self, plotitem, column):
601 return plotitem.points, plotitem.columns[column]
603 def initplotitem(self, plotitem, graph):
605 for xname in self.mathtree.VarList():
606 if xname in graph.axes.keys():
607 if self.xname is None:
610 raise ValueError("multiple variables found")
611 if self.xname is None:
612 raise ValueError("no variable found")
613 plotitem.columns = {self.xname: 0, self.yname: 1}
615 def getcolumnnames(self, plotitem):
616 return [self.xname, self.yname]
618 def adjustaxes(self, plotitem, graph, step):
621 if self.min is not None:
622 points.append(self.min)
623 if self.max is not None:
624 points.append(self.max)
625 for privatedata, style in zip(plotitem.privatedatalist, plotitem.styles):
626 style.adjustaxis(privatedata, plotitem.sharedata, graph, self.xname, points, None)
628 xaxis = graph.axes[self.xname]
629 min, max = xaxis.getrange()
630 if self.min is not None: min = self.min
631 if self.max is not None: max = self.max
632 vmin = xaxis.convert(min)
633 vmax = xaxis.convert(max)
635 for i in range(self.numberofpoints):
636 v = vmin + (vmax-vmin)*i / (self.numberofpoints-1.0)
638 self.context[self.xname] = x
640 y = self.mathtree.Calc(**self.context)
641 except (ArithmeticError, ValueError):
643 plotitem.points.append([x, y])
645 for privatedata, style in zip(plotitem.privatedatalist, plotitem.styles):
646 style.adjustaxis(privatedata, plotitem.sharedata, graph, self.yname, plotitem.points, 1)
649 class paramfunction(_staticdata):
651 defaultstyles = [style.line()]
653 def __init__(self, varname, min, max, expression, title=notitle, points=100, parser=mathtree.parser(), context={}):
655 self.title = expression
658 varlist, expressionlist = expression.split("=")
659 keys = varlist.split(",")
660 mathtrees = parser.parse(expressionlist)
661 if len(keys) != len(mathtrees):
662 raise ValueError("unpack tuple of wrong size")
664 self.points = [None]*points
666 for index, key in enumerate(keys):
667 self.columns[key.strip()] = index
668 for i in range(points):
669 param = min + (max-min)*i / (points-1.0)
670 context[varname] = param
671 self.points[i] = [None]*l
672 for index, mathtree in enumerate(mathtrees):
674 self.points[i][index] = mathtree.Calc(**context)
675 except (ArithmeticError, ValueError):
676 self.points[i][index] = None