graph data: minor corrections + paramfunc
[PyX/mjg.git] / pyx / graph / data.py
blob42a35359c62bb3a761f2128705870497dadd52fa
1 #!/usr/bin/env python
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
31 class _Idata:
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):
39 """Data for a 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
45 data."""
47 def getcolumn(self, column):
48 """Data for a 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)
57 if index is None:
58 return data
59 else:
60 return [point[index] for point in data]
62 def getcount(self):
63 """Number of points
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
75 class variable."""
77 def gettitle(self):
78 """Title of the data
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
84 to be unique."""
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
90 data instance."""
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):
111 """Draw data
113 This method should draw the data."""
115 def key_pt(self, graph, x_pt, y_pt, width_pt, height_pt):
116 """Draw graph key
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."""
123 class styledata:
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"""
129 pass
132 class _data(_Idata):
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:
138 self.data:
139 self.columns:
140 self.styles:
141 self.styledata:
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]
150 def getcount(self):
151 return len(self.data)
153 def gettitle(self):
154 return self.title
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
164 given styles."""
165 provided = [] # already provided styledata variables
166 addstyles = [] # a list of style instances to be added in front
167 for s in styles:
168 for n in s.need:
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."""
179 usedcolumns = []
180 for style in 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):
196 if step == 0:
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):
203 columndataindex = []
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]
209 l = len(data)
210 for column, data, index in columndataindex[1:]:
211 if l != len(data):
212 raise ValueError("data len differs")
213 self.styledata.point = {}
214 for style in self.styles:
215 style.initdrawpoints(self.styledata, graph)
216 for i in xrange(l):
217 for column, data, index in columndataindex:
218 if index is not None:
219 self.styledata.point[column] = data[i][index]
220 else:
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)
232 class list(_data):
233 "Graph data from a list of points"
235 def getcolumndataindex(self, column):
236 try:
237 if self.addlinenumbers:
238 index = self.columns[column]-1
239 else:
240 index = self.columns[column]
241 except KeyError:
242 try:
243 if type(column) != type(column + 0):
244 raise ValueError("integer expected")
245 except:
246 raise ValueError("integer expected")
247 if self.addlinenumbers:
248 if column > 0:
249 index = column-1
250 elif column < 0:
251 index = column
252 else:
253 return range(1, 1+len(self.data)), None
254 else:
255 index = column
256 return self.data, index
258 def __init__(self, data, title="user provided list", addlinenumbers=1, **columns):
259 if len(data):
260 # be paranoid and check each row to have the same number of data
261 l = len(data[0])
262 for p in data[1:]:
263 if l != len(p):
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")
268 self.data = data
269 self.columns = columns
270 self.title = title
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)
283 def VarList(self):
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)
289 return [self]
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
296 def __str__(self):
297 return self.varname
299 def Calc(_hidden_self, **args):
300 return args[_hidden_self.varname]
302 MathTreeFuncsWithCol = mathtree.DefaultMathTreeFuncs + [MathTreeFuncCol]
305 class columntree:
307 def __init__(self, tree):
308 self.tree = tree
309 self.Calc = tree.Calc
310 self.__str__ = tree.__str__
312 def VarList(self):
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)
318 columndict = {}
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:])
325 return columndict
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 ##############################################################
341 class _notitle:
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)"""
346 pass
348 class data(_data):
349 "creates a new data set out of an existing data set"
351 def __init__(self, data, title=_notitle, parser=dataparser(), context={}, **columns):
352 # build a nice title
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])
357 else:
358 self.title = title
360 self.orgdata = data
362 # analyse the **columns argument
363 self.columns = {}
364 newcolumns = {}
365 for column, value in columns.items():
366 try:
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)
373 vardataindex = []
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():
379 try:
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)
386 else:
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]
395 else:
396 vars[var] = data[i]
397 # evaluate expression
398 newdata[i] = tree.Calc(**vars)
399 # we could also do:
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
405 # open issue.
406 self.columns[column] = newdata, None
408 def getcolumndataindex(self, column):
409 return self.columns[column]
411 def getcount(self):
412 return self.orgdata.getcount()
414 def getdefaultstyle(self):
415 return self.orgdata.getdefaultstyle()
418 filecache = {}
420 class file(data):
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"""
433 result = []
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:
438 while len(line):
439 match = stringpattern.match(line)
440 if match:
441 result.append(match.groups()[0])
442 line = line[match.end():]
443 else:
444 match = columnpattern.match(line)
445 if tofloat:
446 try:
447 result.append(float(match.groups()[0]))
448 except (TypeError, ValueError):
449 result.append(match.groups()[0])
450 else:
451 result.append(match.groups()[0])
452 line = line[match.end():]
453 else:
454 if tofloat:
455 try:
456 return map(float, line.split())
457 except (TypeError, ValueError):
458 result = []
459 for r in line.split():
460 try:
461 result.append(float(r))
462 except (TypeError, ValueError):
463 result.append(r)
464 else:
465 return line.split()
466 return result
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,
476 **kwargs):
478 def readfile(file, title):
479 columns = {}
480 points = []
481 linenumber = 0
482 maxcolumns = 0
483 for line in file.readlines():
484 line = line.strip()
485 match = commentpattern.match(line)
486 if match:
487 if not len(points):
488 keys = self.splitline(line[match.end():], stringpattern, columnpattern, tofloat=0)
489 i = 0
490 for key in keys:
491 i += 1 # the first column is number 1 since a linenumber is added in front
492 columns[key] = i
493 else:
494 linedata = []
495 for value in self.splitline(line, stringpattern, columnpattern, tofloat=1):
496 linedata.append(value)
497 if len(linedata):
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)
503 linenumber += 1
504 if skiptail >= every:
505 skip, x = divmod(skiptail, every)
506 del points[-skip:]
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)
512 try:
513 filename.readlines
514 except:
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)
520 else:
521 data.__init__(self, readfile(filename, "user provided file-like object"), **kwargs)
524 conffilecache = {}
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
541 config.readfp(file)
542 sections = config.sections()
543 sections.sort()
544 points = [None]*len(sections)
545 maxcolumns = 1
546 columns = {}
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)
551 try:
552 value = float(value)
553 except:
554 pass
555 try:
556 index = columns[option]
557 except KeyError:
558 columns[option] = maxcolumns
559 point.append(value)
560 maxcolumns += 1
561 else:
562 point[index] = value
563 points[i] = point
564 return list(points, title=title, addlinenumbers=0, **columns)
566 try:
567 filename.readlines
568 except:
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)
573 else:
574 data.__init__(self, readfile(filename, "user provided file-like object"), **kwargs)
577 class _linedata(_data):
579 defaultstyle = [style.line()]
582 # class function:
584 # defaultstyle = style.line()
586 # def __init__(self, expression, title=_notitle, min=None, max=None,
587 # points=100, parser=mathtree.parser(), context={}):
589 # if title is _notitle:
590 # self.title = expression
591 # else:
592 # self.title = title
593 # self.min = min
594 # self.max = max
595 # self.numberofpoints = points
596 # self.context = context.copy() # be save on late evaluations
597 # self.result, expression = [x.strip() for x in expression.split("=")]
598 # self.mathtree = parser.parse(expression)
599 # self.variable = None
601 # def setstyles(self, graph, styles):
602 # self.styles = styles
603 # self.styledata = styledata()
604 # for variable in self.mathtree.VarList():
605 # if variable in graph.axes.keys():
606 # if self.variable is None:
607 # self.variable = variable
608 # else:
609 # raise ValueError("multiple variables found")
610 # if self.variable is None:
611 # raise ValueError("no variable found")
612 # self.xaxis = graph.axes[self.variable]
613 # self.columns = {self.variable: 1, self.result: 2}
614 # unhandledcolumns = self.columns
615 # for style in self.styles:
616 # unhandledcolumns = style.setdata(graph, unhandledcolumns, self.styledata)
617 # unhandledcolumnkeys = unhandledcolumns.keys()
618 # if len(unhandledcolumnkeys):
619 # raise ValueError("style couldn't handle column keys %s" % unhandledcolumnkeys)
621 # def selectstyles(self, graph, selectindex, selecttotal):
622 # for style in self.styles:
623 # style.selectstyle(selectindex, selecttotal, self.styledata)
625 # def adjustaxes(self, graph, step):
626 # """
627 # - on step == 0 axes with fixed data should be adjusted
628 # - on step == 1 the current axes ranges might be used to
629 # calculate further data (e.g. y data for a function y=f(x)
630 # where the y range depends on the x range)
631 # - on step == 2 axes ranges not previously set should be
632 # updated by data accumulated by step 1"""
633 # if step == 0:
634 # self.points = []
635 # if self.min is not None:
636 # self.points.append([None, self.min])
637 # if self.max is not None:
638 # self.points.append([None, self.max])
639 # for style in self.styles:
640 # style.adjustaxes(self.points, [1], self.styledata)
641 # elif step == 1:
642 # min, max = graph.axes[self.variable].getrange()
643 # if self.min is not None: min = self.min
644 # if self.max is not None: max = self.max
645 # vmin = self.xaxis.convert(min)
646 # vmax = self.xaxis.convert(max)
647 # self.points = []
648 # for i in range(self.numberofpoints):
649 # v = vmin + (vmax-vmin)*i / (self.numberofpoints-1.0)
650 # x = self.xaxis.invert(v)
651 # # caution: the virtual coordinate might differ once
652 # # the axis rescales itself to include further ticks etc.
653 # self.points.append([v, x, None])
654 # for point in self.points:
655 # self.context[self.variable] = point[1]
656 # try:
657 # point[2] = self.mathtree.Calc(**self.context)
658 # except (ArithmeticError, ValueError):
659 # pass
660 # elif step == 2:
661 # for style in self.styles:
662 # style.adjustaxes(self.points, [2], self.styledata)
664 # def draw(self, graph):
665 # # TODO code dublication
666 # for style in self.styles:
667 # style.initdrawpoints(graph, self.styledata)
668 # for point in self.points:
669 # self.styledata.point = point
670 # for style in self.styles:
671 # style.drawpoint(graph, self.styledata)
672 # for style in self.styles:
673 # style.donedrawpoints(graph, self.styledata)
676 class paramfunction(_linedata):
678 def __init__(self, varname, min, max, expression, title=_notitle, points=100, parser=mathtree.parser(), context={}):
679 if title is _notitle:
680 self.title = expression
681 else:
682 self.title = title
683 varlist, expressionlist = expression.split("=")
684 keys = varlist.split(",")
685 mathtrees = parser.parse(expressionlist)
686 if len(keys) != len(mathtrees):
687 raise ValueError("unpack tuple of wrong size")
688 self.data = [None]*points
689 emptyresult = [None]*len(keys)
690 self.columns = {}
691 i = 1
692 for key in keys:
693 self.columns[key.strip()] = i
694 i += 1
695 for i in range(points):
696 param = min + (max-min)*i / (points-1.0)
697 context[varname] = param
698 self.data[i] = [param] + emptyresult
699 column = 1
700 for key, column in self.columns.items():
701 self.data[i][column] = mathtrees[column-1].Calc(**context)
702 column += 1