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
28 from pyx
.graph
import style
32 """Interface for graph data
34 Graph data consists in columns, where each column might
35 be identified by a string or an integer. Each row in the
36 resulting table refers to a data point."""
38 def getcolumndataindex(self
, column
):
41 This method returns data of a column by a tuple data, index.
42 column identifies the column. If index is not None, the data
43 of the column is found at position index for each element of
44 the list data. If index is None, the data is the list of
47 def getcolumn(self
, column
):
50 This method returns the data of a column in a list. column
51 has the same meaning as in getcolumndataindex. Note, that
52 this method typically has to create this list, which needs
53 time and memory. While its easy to the user, internally it
54 should be avoided in favor of getcolumndataindex. The method
55 can be implemented as follows:"""
56 data
, index
= self
.getcolumndataindex(column
)
60 return [point
[index
] for point
in data
]
65 This method returns the number of points. All results by
66 getcolumndataindex and getcolumn will fit this number."""
68 def getdefaultstyles(self
):
69 """Default styles for the data
71 Returns a list of default styles for the data. Note to
72 return the same instances when the graph should iterate
73 over the styles using selectstyles. The following default
74 implementation returns the value of the defaultstyle
80 This method returns a title string for the data to be used
81 in graph keys and probably other locations. The method might
82 return None to indicate, that there is no title and the data
83 should be skiped in a graph key. A data title does not need
86 def setstyles(self
, graph
, styles
):
87 """Attach graph styles to data
89 This method is called by the graph to attach styles to the
92 def selectstyles(self
, graph
, selectindex
, selecttotal
):
93 """Perform select on the styles
95 This method should perfrom selectstyle calls on all styles."""
96 for style
in self
.styles
:
97 style
.selectstyle(self
.styledata
, graph
, selectindex
, selecttotal
)
99 def adjustaxes(self
, graph
, step
):
100 """Adjust axes ranges
102 This method should call adjustaxis for all styles.
103 On step == 0 axes with fixed data should be adjusted.
104 On step == 1 the current axes ranges might be used to
105 calculate further data (e.g. y data for a function y=f(x)
106 where the y range depends on the x range). On step == 2
107 axes ranges not previously set should be updated by data
108 accumulated by step 1."""
110 def draw(self
, graph
):
113 This method should draw the data."""
115 def key_pt(self
, graph
, x_pt
, y_pt
, width_pt
, height_pt
):
118 This method should draw a graph key at the given position
119 x_pt, y_pt indicating the lower left corner of the given
120 area width_pt, height_pt."""
124 """Styledata storage class
126 Instances of this class are used to store data from the styles
127 and to pass point data to the styles. is shared
128 between all the style(s) in use by a data instance"""
133 """Partly implements the _Idata interface
135 This class partly implements the _Idata interface. In order
136 to do so, it makes use of various instance variables:
142 self.title: the title of the data
143 self.defaultstyles:"""
145 defaultstyles
= [style
.symbol()]
147 def getcolumndataindex(self
, column
):
148 return self
.data
, self
.columns
[column
]
151 return len(self
.data
)
156 def getdefaultstyles(self
):
157 return self
.defaultstyles
159 def addneededstyles(self
, styles
):
160 """helper method (not part of the interface)
162 This is a helper method, which returns a list of styles where
163 provider styles are added in front to fullfill all needs of the
165 provided
= [] # already provided styledata variables
166 addstyles
= [] # a list of style instances to be added in front
169 if n
not in provided
:
170 addstyles
.append(style
.provider
[n
])
171 provided
.extend(style
.provider
[n
].provide
)
172 provided
.extend(s
.provide
)
173 return addstyles
+ styles
175 def setcolumns(self
, styledata
, graph
, styles
, columns
):
176 """helper method (not part of the interface)
178 This is a helper method to perform setcolumn to all styles."""
181 usedcolumns
.extend(style
.columns(self
.styledata
, graph
, columns
))
182 for column
in columns
:
183 if column
not in usedcolumns
:
184 raise ValueError("unused column '%s'" % column
)
186 def setstyles(self
, graph
, styles
):
187 self
.styledata
= styledata()
188 self
.styles
= self
.addneededstyles(styles
)
189 self
.setcolumns(self
.styledata
, graph
, self
.styles
, self
.columns
.keys())
191 def selectstyles(self
, graph
, selectindex
, selecttotal
):
192 for style
in self
.styles
:
193 style
.selectstyle(self
.styledata
, graph
, selectindex
, selecttotal
)
195 def adjustaxes(self
, graph
, step
):
197 for column
in self
.columns
.keys():
198 data
, index
= self
.getcolumndataindex(column
)
199 for style
in self
.styles
:
200 style
.adjustaxis(self
.styledata
, graph
, column
, data
, index
)
202 def draw(self
, graph
):
204 for column
in self
.columns
.keys():
205 data
, index
= self
.getcolumndataindex(column
)
206 columndataindex
.append((column
, data
, index
))
207 if len(columndataindex
):
208 column
, data
, index
= columndataindex
[0]
210 for column
, data
, index
in columndataindex
[1:]:
212 raise ValueError("data len differs")
213 self
.styledata
.point
= {}
214 for style
in self
.styles
:
215 style
.initdrawpoints(self
.styledata
, graph
)
217 for column
, data
, index
in columndataindex
:
218 if index
is not None:
219 self
.styledata
.point
[column
] = data
[i
][index
]
221 self
.styledata
.point
[column
] = data
[i
]
222 for style
in self
.styles
:
223 style
.drawpoint(self
.styledata
, graph
)
224 for style
in self
.styles
:
225 style
.donedrawpoints(self
.styledata
, graph
)
227 def key_pt(self
, graph
, x_pt
, y_pt
, width_pt
, height_pt
):
228 for style
in self
.styles
:
229 style
.key_pt(self
.styledata
, graph
, x_pt
, y_pt
, width_pt
, height_pt
)
233 "Graph data from a list of points"
235 def getcolumndataindex(self
, column
):
237 if self
.addlinenumbers
:
238 index
= self
.columns
[column
]-1
240 index
= self
.columns
[column
]
243 if type(column
) != type(column
+ 0):
244 raise ValueError("integer expected")
246 raise ValueError("integer expected")
247 if self
.addlinenumbers
:
253 return range(1, 1+len(self
.data
)), None
256 return self
.data
, index
258 def __init__(self
, data
, title
="user provided list", addlinenumbers
=1, **columns
):
260 # be paranoid and check each row to have the same number of data
264 raise ValueError("different number of columns per point")
265 for v
in columns
.values():
266 if abs(v
) > l
or (not addlinenumbers
and abs(v
) == l
):
267 raise ValueError("column number bigger than number of columns")
269 self
.columns
= columns
271 self
.addlinenumbers
= addlinenumbers
274 ##############################################################
275 # math tree enhanced by column number variables
276 ##############################################################
278 class MathTreeFuncCol(mathtree
.MathTreeFunc1
):
280 def __init__(self
, *args
):
281 mathtree
.MathTreeFunc1
.__init__(self
, "_column_", *args
)
284 # we misuse VarList here:
285 # - instead of returning a string, we return this instance itself
286 # - before calculating the expression, you must call ColumnNameAndNumber
287 # once (when limiting the context to external defined variables,
288 # otherwise you have to call it each time)
291 def ColumnNameAndNumber(_hidden_self
, **args
):
292 number
= int(_hidden_self
.Args
[0].Calc(**args
))
293 _hidden_self
.varname
= "_column_%i" % number
294 return _hidden_self
.varname
, number
299 def Calc(_hidden_self
, **args
):
300 return args
[_hidden_self
.varname
]
302 MathTreeFuncsWithCol
= mathtree
.DefaultMathTreeFuncs
+ [MathTreeFuncCol
]
307 def __init__(self
, tree
):
309 self
.Calc
= tree
.Calc
310 self
.__str
__ = tree
.__str
__
313 # returns a list of regular variables (strings) like the original mathtree
314 return [var
for var
in self
.tree
.VarList() if not isinstance(var
, MathTreeFuncCol
) and var
[:8] != "_column_"]
316 def columndict(_hidden_self
, **context
):
317 # returns a dictionary of column names (keys) and column numbers (values)
319 for var
in _hidden_self
.tree
.VarList():
320 if isinstance(var
, MathTreeFuncCol
):
321 name
, number
= var
.ColumnNameAndNumber(**context
)
322 columndict
[name
] = number
323 elif var
[:8] == "_column_":
324 columndict
[var
] = int(var
[8:])
328 class dataparser(mathtree
.parser
):
329 # mathtree parser enhanced by column handling
330 # parse returns a columntree instead of a regular tree
332 def __init__(self
, MathTreeFuncs
=MathTreeFuncsWithCol
, **kwargs
):
333 mathtree
.parser
.__init
__(self
, MathTreeFuncs
=MathTreeFuncs
, **kwargs
)
335 def parse(self
, expr
):
336 return columntree(mathtree
.parser
.parse(self
, expr
.replace("$", "_column_")))
338 ##############################################################
342 """this is a helper class to mark, that no title was privided
343 (since a title equals None is a valid input, it needs to be
344 distinguished from providing no title when a title will be
345 created automatically)"""
349 "creates a new data set out of an existing data set"
351 def __init__(self
, data
, title
=_notitle
, parser
=dataparser(), context
={}, **columns
):
353 if title
is _notitle
:
354 items
= columns
.items()
355 items
.sort() # we want sorted items (otherwise they would be unpredictable scrambled)
356 self
.title
= data
.title
+ ": " + ", ".join(["%s=%s" % item
for item
in items
])
362 # analyse the **columns argument
365 for column
, value
in columns
.items():
367 # try if it is a valid column identifier
368 self
.columns
[column
] = self
.orgdata
.getcolumndataindex(value
)
369 except (KeyError, ValueError):
370 # take it as a mathematical expression
371 tree
= parser
.parse(value
)
372 columndict
= tree
.columndict(**context
)
374 for var
, value
in columndict
.items():
375 # column data accessed via $<column number>
376 data
, index
= self
.orgdata
.getcolumndataindex(value
)
377 vardataindex
.append((var
, data
, index
))
378 for var
in tree
.VarList():
380 # column data accessed via the name of the column
381 data
, index
= self
.orgdata
.getcolumndataindex(var
)
382 except (KeyError, ValueError):
383 # other data available in context
384 if var
not in context
.keys():
385 raise ValueError("undefined variable '%s'" % var
)
387 vardataindex
.append((var
, data
, index
))
388 newdata
= [None]*self
.getcount()
389 vars = context
.copy() # do not modify context, use a copy vars instead
390 for i
in xrange(self
.getcount()):
391 # insert column data as prepared in vardataindex
392 for var
, data
, index
in vardataindex
:
393 if index
is not None:
394 vars[var
] = data
[i
][index
]
397 # evaluate expression
398 newdata
[i
] = tree
.Calc(**vars)
400 # point[newcolumnnumber] = eval(str(tree), vars)
402 # TODO: It might happen, that the evaluation of the expression
403 # seems to work, but the result is NaN. It depends on the
404 # plattform and configuration. The NaN handling is an
406 self
.columns
[column
] = newdata
, None
408 def getcolumndataindex(self
, column
):
409 return self
.columns
[column
]
412 return self
.orgdata
.getcount()
414 def getdefaultstyle(self
):
415 return self
.orgdata
.getdefaultstyle()
422 defaultcommentpattern
= re
.compile(r
"(#+|!+|%+)\s*")
423 defaultstringpattern
= re
.compile(r
"\"(.*?
)\"(\s
+|$
)")
424 defaultcolumnpattern = re.compile(r"(.*?
)(\s
+|$
)")
426 def splitline(self, line, stringpattern, columnpattern, tofloat=1):
427 """returns a tuple created out of the string line
428 - matches stringpattern and columnpattern, adds the first group of that
429 match to the result and and removes those matches until the line is empty
430 - when stringpattern matched, the result is always kept as a string
431 - when columnpattern matched and tofloat is true, a conversion to a float
432 is tried; when this conversion fails, the string is kept"""
434 # try to gain speed by skip matching regular expressions
435 if line.find('"')!=-1 or \
436 stringpattern is not self.defaultstringpattern or \
437 columnpattern is not self.defaultcolumnpattern:
439 match = stringpattern.match(line)
441 result.append(match.groups()[0])
442 line = line[match.end():]
444 match = columnpattern.match(line)
447 result.append(float(match.groups()[0]))
448 except (TypeError, ValueError):
449 result.append(match.groups()[0])
451 result.append(match.groups()[0])
452 line = line[match.end():]
456 return map(float, line.split())
457 except (TypeError, ValueError):
459 for r in line.split():
461 result.append(float(r))
462 except (TypeError, ValueError):
468 def getcachekey(self, *args):
469 return ":".join([str(x) for x in args])
471 def __init__(self, filename,
472 commentpattern=defaultcommentpattern,
473 stringpattern=defaultstringpattern,
474 columnpattern=defaultcolumnpattern,
475 skiphead=0, skiptail=0, every=1,
478 def readfile(file, title):
483 for line in file.readlines():
485 match = commentpattern.match(line)
488 keys = self.splitline(line[match.end():], stringpattern, columnpattern, tofloat=0)
491 i += 1 # the first column is number 1 since a linenumber is added in front
495 for value in self.splitline(line, stringpattern, columnpattern, tofloat=1):
496 linedata.append(value)
498 if linenumber >= skiphead and not ((linenumber - skiphead) % every):
499 linedata = [linenumber + 1] + linedata
500 if len(linedata) > maxcolumns:
501 maxcolumns = len(linedata)
502 points.append(linedata)
504 if skiptail >= every:
505 skip, x = divmod(skiptail, every)
507 for i in xrange(len(points)):
508 if len(points[i]) != maxcolumns:
509 points[i].extend([None]*(maxcolumns-len(points[i])))
510 return list(points, title=title, addlinenumbers=0, **columns)
515 # not a file-like object -> open it
516 cachekey = self.getcachekey(filename, commentpattern, stringpattern, columnpattern, skiphead, skiptail, every)
517 if not filecache.has_key(cachekey):
518 filecache[cachekey] = readfile(open(filename), filename)
519 data.__init__(self, filecache[cachekey], **kwargs)
521 data.__init__(self, readfile(filename, "user provided file-like object"), **kwargs)
526 class conffile(data):
528 def __init__(self, filename, **kwargs):
529 """read data from a config-like file
530 - filename is a string
531 - each row is defined by a section in the config-like file (see
532 config module description)
533 - the columns for each row are defined by lines in the section file;
534 the option entries identify and name the columns
535 - further keyword arguments are passed to the constructor of data,
536 keyword arguments data and titles excluded"""
538 def readfile(file, title):
539 config = ConfigParser.ConfigParser()
540 config.optionxform = str
542 sections = config.sections()
544 points = [None]*len(sections)
547 for i in xrange(len(sections)):
548 point = [sections[i]] + [None]*(maxcolumns-1)
549 for option in config.options(sections[i]):
550 value = config.get(sections[i], option)
556 index = columns[option]
558 columns[option] = maxcolumns
564 return list(points, title=title, addlinenumbers=0, **columns)
569 # not a file-like object -> open it
570 if not filecache.has_key(filename):
571 filecache[filename] = readfile(open(filename), filename)
572 data.__init__(self, filecache[filename], **kwargs)
574 data.__init__(self, readfile(filename, "user provided file-like object"), **kwargs)
577 class _linedata(_data):
579 defaultstyle = [style.line()]
582 class function(_linedata):
584 def __init__(self, expression, title=_notitle, min=None, max=None,
585 points=100, parser=mathtree.parser(), context={}):
587 if title is _notitle:
588 self.title = expression
593 self.numberofpoints = points
594 self.context = context.copy() # be save on late evaluations
595 self.yname, expression = [x.strip() for x in expression.split("=")]
596 self.mathtree = parser.parse(expression)
598 def setstyles(self, graph, styles):
600 for xname in self.mathtree.VarList():
601 if xname in graph.axes.keys():
602 if self.xname is None:
605 raise ValueError("multiple variables found")
606 if self.xname is None:
607 raise ValueError("no variable found")
608 self.columns = {self.xname: 0, self.yname: 1}
609 _linedata.setstyles(self, graph, styles)
611 def adjustaxes(self, graph, step):
614 if self.min is not None:
615 self.points.append(self.min)
616 if self.max is not None:
617 self.points.append(self.max)
618 for style in self.styles:
619 style.adjustaxis(self.styledata, graph, self.xname, data, None)
621 xaxis = graph.axes[self.xname]
622 min, max = xaxis.getrange()
623 if self.min is not None: min = self.min
624 if self.max is not None: max = self.max
625 vmin = xaxis.convert(min)
626 vmax = xaxis.convert(max)
628 for i in range(self.numberofpoints):
629 v = vmin + (vmax-vmin)*i / (self.numberofpoints-1.0)
631 self.context[self.xname] = x
633 y = self.mathtree.Calc(**self.context)
634 except (ArithmeticError, ValueError):
636 self.data.append([x, y])
638 for style in self.styles:
639 style.adjustaxis(self.styledata, graph, self.yname, self.data, 1)
642 class paramfunction(_linedata):
644 def __init__(self, varname, min, max, expression, title=_notitle, points=100, parser=mathtree.parser(), context={}):
645 if title is _notitle:
646 self.title = expression
649 varlist, expressionlist = expression.split("=")
650 keys = varlist.split(",")
651 mathtrees = parser.parse(expressionlist)
652 if len(keys) != len(mathtrees):
653 raise ValueError("unpack tuple of wrong size")
655 self.data = [None]*points
657 for index, key in enumerate(keys):
658 self.columns[key.strip()] = index
659 for i in range(points):
660 param = min + (max-min)*i / (points-1.0)
661 context[varname] = param
662 self.data[i] = [None]*l
663 for index, mathtree in enumerate(mathtrees):
664 self.data[i][index] = mathtree.Calc(**context)