- adjustments to the new graph data+style handling
[PyX/mjg.git] / pyx / graph / data.py
blob9c78d9024d2bb2c6b978a394fc1a6c964808ae73
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, text
28 from pyx.graph import style
30 try:
31 enumerate([])
32 except NameError:
33 # fallback implementation for Python 2.2. and below
34 def enumerate(list):
35 return zip(xrange(len(list)), list)
37 class _Idata:
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):
50 """Data for a 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
56 data.
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):
65 """Data for a 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)
74 def getcount(self):
75 """Number of points
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)
90 def gettitle(self):
91 """Title of the data
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
97 to be unique."""
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):
140 """Draw data
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)
148 class _data(_Idata):
149 """Partly implements the _Idata interface"""
151 defaultstyles = [style.symbol()]
153 def getcolumn(self, column):
154 data, index = self.getcolumnpointsindex(column)
155 if index is None:
156 return data
157 else:
158 return [point[index] for point in data]
160 def getdefaultstyles(self):
161 return self.defaultstyles
163 def gettitle(self):
164 return self.title
166 def initplotitem(self, plotitem, graph):
167 pass
169 def draw(self, plotitem, graph):
170 columnpointsindex = []
171 l = None
172 for column in self.getcolumnnames(plotitem):
173 points, index = self.getcolumnpointsindex_plotitem(plotitem, column)
174 columnpointsindex.append((column, points, index))
175 if l is None:
176 l = len(points)
177 else:
178 if l != len(points):
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 = {}
184 for i in xrange(l):
185 for column, points, index in columnpointsindex:
186 if index is not None:
187 plotitem.sharedata.point[column] = points[i][index]
188 else:
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]
205 def getcount(self):
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):
215 if step == 0:
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")
227 def getcount(self):
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):
235 try:
236 if self.addlinenumbers:
237 index = self.columns[column]-1
238 else:
239 index = self.columns[column]
240 except KeyError:
241 try:
242 if type(column) != type(column + 0):
243 raise ValueError("integer expected")
244 except:
245 raise ValueError("integer expected")
246 if self.addlinenumbers:
247 if column > 0:
248 index = column-1
249 elif column < 0:
250 index = column
251 else:
252 return range(1, 1+len(self.points)), None
253 else:
254 index = column
255 return self.points, index
257 def __init__(self, points, title="user provided list", addlinenumbers=1, **columns):
258 if len(points):
259 # be paranoid and check each row to have the same number of points
260 l = len(points[0])
261 for p in points[1:]:
262 if l != len(p):
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")
267 self.points = points
268 self.columns = columns
269 self.title = title
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)
282 def VarList(self):
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)
288 return [self]
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
295 def __str__(self):
296 return self.varname
298 def Calc(_hidden_self, **args):
299 return args[_hidden_self.varname]
301 MathTreeFuncsWithCol = mathtree.DefaultMathTreeFuncs + [MathTreeFuncCol]
304 class columntree:
306 def __init__(self, tree):
307 self.tree = tree
308 self.Calc = tree.Calc
309 self.__str__ = tree.__str__
311 def VarList(self):
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)
317 columndict = {}
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:])
324 return columndict
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 ##############################################################
340 class notitle:
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)"""
345 pass
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):
351 # build a nice title
352 if title is notitle:
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]))
359 else:
360 self.title = title
362 self.orgdata = data
364 # analyse the **columns argument
365 self.columns = {}
366 newcolumns = {}
367 for column, value in columns.items():
368 try:
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)
375 varpointsindex = []
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():
381 try:
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)
388 else:
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]
397 else:
398 vars[var] = points[i]
399 # evaluate expression
400 try:
401 newdata[i] = tree.Calc(**vars)
402 except (ArithmeticError, ValueError):
403 newdata[i] = None
404 # we could also do:
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]
416 def getcount(self):
417 return self.orgdata.getcount()
419 def getdefaultstyles(self):
420 return self.orgdata.getdefaultstyles()
423 filecache = {}
425 class file(data):
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"""
438 result = []
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:
443 while len(line):
444 match = stringpattern.match(line)
445 if match:
446 result.append(match.groups()[0])
447 line = line[match.end():]
448 else:
449 match = columnpattern.match(line)
450 if tofloat:
451 try:
452 result.append(float(match.groups()[0]))
453 except (TypeError, ValueError):
454 result.append(match.groups()[0])
455 else:
456 result.append(match.groups()[0])
457 line = line[match.end():]
458 else:
459 if tofloat:
460 try:
461 return map(float, line.split())
462 except (TypeError, ValueError):
463 result = []
464 for r in line.split():
465 try:
466 result.append(float(r))
467 except (TypeError, ValueError):
468 result.append(r)
469 else:
470 return line.split()
471 return result
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,
481 **kwargs):
483 def readfile(file, title):
484 columns = {}
485 points = []
486 linenumber = 0
487 maxcolumns = 0
488 for line in file.readlines():
489 line = line.strip()
490 match = commentpattern.match(line)
491 if match:
492 if not len(points):
493 keys = self.splitline(line[match.end():], stringpattern, columnpattern, tofloat=0)
494 i = 0
495 for key in keys:
496 i += 1 # the first column is number 1 since a linenumber is added in front
497 columns[key] = i
498 else:
499 linedata = []
500 for value in self.splitline(line, stringpattern, columnpattern, tofloat=1):
501 linedata.append(value)
502 if len(linedata):
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)
508 linenumber += 1
509 if skiptail >= every:
510 skip, x = divmod(skiptail, every)
511 del points[-skip:]
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)
517 try:
518 filename.readlines
519 except:
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)
525 else:
526 data.__init__(self, readfile(filename, "user provided file-like object"), **kwargs)
529 conffilecache = {}
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
546 config.readfp(file)
547 sections = config.sections()
548 sections.sort()
549 points = [None]*len(sections)
550 maxcolumns = 1
551 columns = {}
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)
556 try:
557 value = float(value)
558 except:
559 pass
560 try:
561 index = columns[option]
562 except KeyError:
563 columns[option] = maxcolumns
564 point.append(value)
565 maxcolumns += 1
566 else:
567 point[index] = value
568 points[i] = point
569 return list(points, title=title, addlinenumbers=0, **columns)
571 try:
572 filename.readlines
573 except:
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)
578 else:
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={}):
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.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):
604 self.xname = None
605 for xname in self.mathtree.VarList():
606 if xname in graph.axes.keys():
607 if self.xname is None:
608 self.xname = xname
609 else:
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):
619 if step == 0:
620 points = []
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)
627 elif step == 1:
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)
634 plotitem.points = []
635 for i in range(self.numberofpoints):
636 v = vmin + (vmax-vmin)*i / (self.numberofpoints-1.0)
637 x = xaxis.invert(v)
638 self.context[self.xname] = x
639 try:
640 y = self.mathtree.Calc(**self.context)
641 except (ArithmeticError, ValueError):
642 y = None
643 plotitem.points.append([x, y])
644 elif step == 2:
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={}):
654 if title is notitle:
655 self.title = expression
656 else:
657 self.title = title
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")
663 l = len(keys)
664 self.points = [None]*points
665 self.columns = {}
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):
673 try:
674 self.points[i][index] = mathtree.Calc(**context)
675 except (ArithmeticError, ValueError):
676 self.points[i][index] = None