- plotitem acts as a container for data now (i.e. data instances
[PyX/mjg.git] / pyx / graph / data.py
blob4c7f5288d98a2872da63c27da1ee7efb8a75193f
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 All methods except for the constructor should consider
39 self to be readonly, since the data instance might be shared
40 between several graphs simultaniously. The plotitem instance
41 created by the graph is available as a container class."""
43 def getcolumnpointsindex(self, column):
44 """Data for a column
46 This method returns data of a column by a tuple data, index.
47 column identifies the column. If index is not None, the data
48 of the column is found at position index for each element of
49 the list data. If index is None, the data is the list of
50 data.
52 Some data might not be available by this function since it
53 is dynamic, i.e. it depends on plotitem. An example are
54 function data, which is available within a graph only. Thus
55 this method might raise an exception."""
56 raise NotImplementedError("call to an abstract method of %r" % self)
58 def getcolumn(self, column):
59 """Data for a column
61 This method returns the data of a column in a list. column
62 has the same meaning as in getcolumnpointsindex. Note, that
63 this method typically has to create a new list, which needs
64 time and memory. While its easy to the user, internally
65 it should be avoided in favor of getcolumnpointsindex."""
66 raise NotImplementedError("call to an abstract method of %r" % self)
68 def getcount(self):
69 """Number of points
71 This method returns the number of points. All results by
72 getcolumnpointsindex and getcolumn will fit this number.
73 It might raise an exception as getcolumnpointsindex."""
74 raise NotImplementedError("call to an abstract method of %r" % self)
76 def getdefaultstyles(self):
77 """Default styles for the data
79 Returns a list of default styles for the data. Note to
80 return the same instances when the graph should iterate
81 over the styles using selectstyles."""
82 raise NotImplementedError("call to an abstract method of %r" % self)
84 def gettitle(self):
85 """Title of the data
87 This method returns a title string for the data to be used
88 in graph keys and probably other locations. The method might
89 return None to indicate, that there is no title and the data
90 should be skiped in a graph key. Data titles does not need
91 to be unique."""
92 raise NotImplementedError("call to an abstract method of %r" % self)
94 def initplotitem(self, plotitem, graph):
95 """Initialize plotitem
97 This function within the plotitem initialization procedure
98 and allows to initialize the plotitem as a data container.
99 For static data the method might just do nothing."""
100 raise NotImplementedError("call to an abstract method of %r" % self)
102 def getcolumnpointsindex_plotitem(self, plotitem, column):
103 """Data for a column with plotitem
105 Like getcolumnpointsindex but for use within a graph, i.e. with
106 a plotitem container class. For static data being defined within
107 the constructor already, the plotitem reference is not needed and
108 the method can be implemented by calling getcolumnpointsindex."""
109 raise NotImplementedError("call to an abstract method of %r" % self)
111 def getcolumnnames(self, plotitem):
112 """Return list of column names of the data
114 This method returns a list of column names. It might
115 depend on the graph. (*YES*, it might depend on the graph
116 in case of a function, where function variables might be
117 axis names. Other variables, not available by axes, will
118 be taken from the context.)"""
119 raise NotImplementedError("call to an abstract method of %r" % self)
121 def adjustaxes(self, plotitem, graph, step):
122 """Adjust axes ranges
124 This method should call adjustaxis for all styles.
125 On step == 0 axes with fixed data should be adjusted.
126 On step == 1 the current axes ranges might be used to
127 calculate further data (e.g. y data for a function y=f(x)
128 where the y range depends on the x range). On step == 2
129 axes ranges not previously set should be updated by data
130 accumulated by step 1."""
131 raise NotImplementedError("call to an abstract method of %r" % self)
133 def draw(self, plotitem, graph):
134 """Draw data
136 This method should draw the data. Its called by plotinfo,
137 since it can be implemented here more efficiently by avoiding
138 some additional function calls."""
139 raise NotImplementedError("call an abstract method of %r" % self)
141 # def key_pt(self, plotitem, graph, x_pt, y_pt, width_pt, height_pt, dy_pt):
142 # """Draw graph key
144 # This method should draw a graph key at the given position
145 # x_pt, y_pt indicating the lower left corner of the given
146 # area width_pt, height_pt. The styles might draw several
147 # key entries shifted vertically by dy_pt. The method returns
148 # the number of key entries."""
151 class _data(_Idata):
152 """Partly implements the _Idata interface"""
154 defaultstyles = [style.symbol()]
156 def getcolumn(self, column):
157 data, index = self.getcolumnpointsindex(column)
158 if index is None:
159 return data
160 else:
161 return [point[index] for point in data]
163 def getdefaultstyles(self):
164 return self.defaultstyles
166 def gettitle(self):
167 return self.title
169 def initplotitem(self, plotitem, graph):
170 pass
172 def draw(self, plotitem, graph):
173 columnpointsindex = []
174 l = None
175 for column in self.getcolumnnames(plotitem):
176 points, index = self.getcolumnpointsindex_plotitem(plotitem, column)
177 columnpointsindex.append((column, points, index))
178 if l is None:
179 l = len(points)
180 else:
181 if l != len(points):
182 raise ValueError("points len differs")
183 for privatedata, style in zip(plotitem.privatedatalist, plotitem.styles):
184 style.initdrawpoints(privatedata, plotitem.sharedata, graph)
185 if len(columnpointsindex):
186 plotitem.sharedata.point = {}
187 for i in xrange(l):
188 for column, points, index in columnpointsindex:
189 if index is not None:
190 plotitem.sharedata.point[column] = points[i][index]
191 else:
192 plotitem.sharedata.point[column] = points[i]
193 for privatedata, style in zip(plotitem.privatedatalist, plotitem.styles):
194 style.drawpoint(privatedata, plotitem.sharedata, graph)
195 for privatedata, style in zip(plotitem.privatedatalist, plotitem.styles):
196 style.donedrawpoints(privatedata, plotitem.sharedata, graph)
199 class _staticdata(_data):
200 """Partly implements the _Idata interface
202 This class partly implements the _Idata interface for static data
203 using self.columns and self.points to be initialized by the constructor."""
205 def getcolumnpointsindex(self, column):
206 return self.points, self.columns[column]
208 def getcount(self):
209 return len(self.points)
211 def getcolumnnames(self, plotitem):
212 return self.columns.keys()
214 def getcolumnpointsindex_plotitem(self, plotitem, column):
215 return self.getcolumnpointsindex(column)
217 def adjustaxes(self, plotitem, graph, step):
218 if step == 0:
219 for column in self.getcolumnnames(plotitem):
220 points, index = self.getcolumnpointsindex_plotitem(plotitem, column)
221 for privatedata, style in zip(plotitem.privatedatalist, plotitem.styles):
222 style.adjustaxis(privatedata, plotitem.sharedata, graph, column, points, index)
225 class _dynamicdata(_data):
227 def getcolumnpointsindex(self, column):
228 raise RuntimeError("dynamic data not available outside a graph")
230 def getcount(self):
231 raise RuntimeError("dynamic data typically has no fixed number of points")
234 class list(_staticdata):
235 "Graph data from a list of points"
237 def getcolumnpointsindex(self, column):
238 try:
239 if self.addlinenumbers:
240 index = self.columns[column]-1
241 else:
242 index = self.columns[column]
243 except KeyError:
244 try:
245 if type(column) != type(column + 0):
246 raise ValueError("integer expected")
247 except:
248 raise ValueError("integer expected")
249 if self.addlinenumbers:
250 if column > 0:
251 index = column-1
252 elif column < 0:
253 index = column
254 else:
255 return range(1, 1+len(self.points)), None
256 else:
257 index = column
258 return self.points, index
260 def __init__(self, points, title="user provided list", addlinenumbers=1, **columns):
261 if len(points):
262 # be paranoid and check each row to have the same number of points
263 l = len(points[0])
264 for p in points[1:]:
265 if l != len(p):
266 raise ValueError("different number of columns per point")
267 for v in columns.values():
268 if abs(v) > l or (not addlinenumbers and abs(v) == l):
269 raise ValueError("column number bigger than number of columns")
270 self.points = points
271 self.columns = columns
272 self.title = title
273 self.addlinenumbers = addlinenumbers
276 ##############################################################
277 # math tree enhanced by column number variables
278 ##############################################################
280 class MathTreeFuncCol(mathtree.MathTreeFunc1):
282 def __init__(self, *args):
283 mathtree.MathTreeFunc1.__init__(self, "_column_", *args)
285 def VarList(self):
286 # we misuse VarList here:
287 # - instead of returning a string, we return this instance itself
288 # - before calculating the expression, you must call ColumnNameAndNumber
289 # once (when limiting the context to external defined variables,
290 # otherwise you have to call it each time)
291 return [self]
293 def ColumnNameAndNumber(_hidden_self, **args):
294 number = int(_hidden_self.Args[0].Calc(**args))
295 _hidden_self.varname = "_column_%i" % number
296 return _hidden_self.varname, number
298 def __str__(self):
299 return self.varname
301 def Calc(_hidden_self, **args):
302 return args[_hidden_self.varname]
304 MathTreeFuncsWithCol = mathtree.DefaultMathTreeFuncs + [MathTreeFuncCol]
307 class columntree:
309 def __init__(self, tree):
310 self.tree = tree
311 self.Calc = tree.Calc
312 self.__str__ = tree.__str__
314 def VarList(self):
315 # returns a list of regular variables (strings) like the original mathtree
316 return [var for var in self.tree.VarList() if not isinstance(var, MathTreeFuncCol) and var[:8] != "_column_"]
318 def columndict(_hidden_self, **context):
319 # returns a dictionary of column names (keys) and column numbers (values)
320 columndict = {}
321 for var in _hidden_self.tree.VarList():
322 if isinstance(var, MathTreeFuncCol):
323 name, number = var.ColumnNameAndNumber(**context)
324 columndict[name] = number
325 elif var[:8] == "_column_":
326 columndict[var] = int(var[8:])
327 return columndict
330 class dataparser(mathtree.parser):
331 # mathtree parser enhanced by column handling
332 # parse returns a columntree instead of a regular tree
334 def __init__(self, MathTreeFuncs=MathTreeFuncsWithCol, **kwargs):
335 mathtree.parser.__init__(self, MathTreeFuncs=MathTreeFuncs, **kwargs)
337 def parse(self, expr):
338 return columntree(mathtree.parser.parse(self, expr.replace("$", "_column_")))
340 ##############################################################
343 class notitle:
344 """this is a helper class to mark, that no title was privided
345 (since a title equals None is a valid input, it needs to be
346 distinguished from providing no title when a title will be
347 created automatically)"""
348 pass
350 class data(_staticdata):
351 "creates a new data set out of an existing data set"
353 def __init__(self, data, title=notitle, parser=dataparser(), context={}, **columns):
354 # build a nice title
355 if title is notitle:
356 items = columns.items()
357 items.sort() # we want sorted items (otherwise they would be unpredictable scrambled)
358 self.title = data.title + ": " + ", ".join(["%s=%s" % item for item 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 newdata[i] = tree.Calc(**vars)
401 # we could also do:
402 # point[newcolumnnumber] = eval(str(tree), vars)
404 # XXX: It might happen, that the evaluation of the expression
405 # seems to work, but the result is NaN/Inf/-Inf. This
406 # is highly plattform dependend.
408 self.columns[column] = newdata, None
410 def getcolumnpointsindex(self, column):
411 return self.columns[column]
413 def getcount(self):
414 return self.orgdata.getcount()
416 def getdefaultstyles(self):
417 return self.orgdata.getdefaultstyles()
420 filecache = {}
422 class file(data):
424 defaultcommentpattern = re.compile(r"(#+|!+|%+)\s*")
425 defaultstringpattern = re.compile(r"\"(.*?)\"(\s+|$)")
426 defaultcolumnpattern = re.compile(r"(.*?)(\s+|$)")
428 def splitline(self, line, stringpattern, columnpattern, tofloat=1):
429 """returns a tuple created out of the string line
430 - matches stringpattern and columnpattern, adds the first group of that
431 match to the result and and removes those matches until the line is empty
432 - when stringpattern matched, the result is always kept as a string
433 - when columnpattern matched and tofloat is true, a conversion to a float
434 is tried; when this conversion fails, the string is kept"""
435 result = []
436 # try to gain speed by skip matching regular expressions
437 if line.find('"')!=-1 or \
438 stringpattern is not self.defaultstringpattern or \
439 columnpattern is not self.defaultcolumnpattern:
440 while len(line):
441 match = stringpattern.match(line)
442 if match:
443 result.append(match.groups()[0])
444 line = line[match.end():]
445 else:
446 match = columnpattern.match(line)
447 if tofloat:
448 try:
449 result.append(float(match.groups()[0]))
450 except (TypeError, ValueError):
451 result.append(match.groups()[0])
452 else:
453 result.append(match.groups()[0])
454 line = line[match.end():]
455 else:
456 if tofloat:
457 try:
458 return map(float, line.split())
459 except (TypeError, ValueError):
460 result = []
461 for r in line.split():
462 try:
463 result.append(float(r))
464 except (TypeError, ValueError):
465 result.append(r)
466 else:
467 return line.split()
468 return result
470 def getcachekey(self, *args):
471 return ":".join([str(x) for x in args])
473 def __init__(self, filename,
474 commentpattern=defaultcommentpattern,
475 stringpattern=defaultstringpattern,
476 columnpattern=defaultcolumnpattern,
477 skiphead=0, skiptail=0, every=1,
478 **kwargs):
480 def readfile(file, title):
481 columns = {}
482 points = []
483 linenumber = 0
484 maxcolumns = 0
485 for line in file.readlines():
486 line = line.strip()
487 match = commentpattern.match(line)
488 if match:
489 if not len(points):
490 keys = self.splitline(line[match.end():], stringpattern, columnpattern, tofloat=0)
491 i = 0
492 for key in keys:
493 i += 1 # the first column is number 1 since a linenumber is added in front
494 columns[key] = i
495 else:
496 linedata = []
497 for value in self.splitline(line, stringpattern, columnpattern, tofloat=1):
498 linedata.append(value)
499 if len(linedata):
500 if linenumber >= skiphead and not ((linenumber - skiphead) % every):
501 linedata = [linenumber + 1] + linedata
502 if len(linedata) > maxcolumns:
503 maxcolumns = len(linedata)
504 points.append(linedata)
505 linenumber += 1
506 if skiptail >= every:
507 skip, x = divmod(skiptail, every)
508 del points[-skip:]
509 for i in xrange(len(points)):
510 if len(points[i]) != maxcolumns:
511 points[i].extend([None]*(maxcolumns-len(points[i])))
512 return list(points, title=title, addlinenumbers=0, **columns)
514 try:
515 filename.readlines
516 except:
517 # not a file-like object -> open it
518 cachekey = self.getcachekey(filename, commentpattern, stringpattern, columnpattern, skiphead, skiptail, every)
519 if not filecache.has_key(cachekey):
520 filecache[cachekey] = readfile(open(filename), filename)
521 data.__init__(self, filecache[cachekey], **kwargs)
522 else:
523 data.__init__(self, readfile(filename, "user provided file-like object"), **kwargs)
526 conffilecache = {}
528 class conffile(data):
530 def __init__(self, filename, **kwargs):
531 """read data from a config-like file
532 - filename is a string
533 - each row is defined by a section in the config-like file (see
534 config module description)
535 - the columns for each row are defined by lines in the section file;
536 the option entries identify and name the columns
537 - further keyword arguments are passed to the constructor of data,
538 keyword arguments data and titles excluded"""
540 def readfile(file, title):
541 config = ConfigParser.ConfigParser()
542 config.optionxform = str
543 config.readfp(file)
544 sections = config.sections()
545 sections.sort()
546 points = [None]*len(sections)
547 maxcolumns = 1
548 columns = {}
549 for i in xrange(len(sections)):
550 point = [sections[i]] + [None]*(maxcolumns-1)
551 for option in config.options(sections[i]):
552 value = config.get(sections[i], option)
553 try:
554 value = float(value)
555 except:
556 pass
557 try:
558 index = columns[option]
559 except KeyError:
560 columns[option] = maxcolumns
561 point.append(value)
562 maxcolumns += 1
563 else:
564 point[index] = value
565 points[i] = point
566 return list(points, title=title, addlinenumbers=0, **columns)
568 try:
569 filename.readlines
570 except:
571 # not a file-like object -> open it
572 if not filecache.has_key(filename):
573 filecache[filename] = readfile(open(filename), filename)
574 data.__init__(self, filecache[filename], **kwargs)
575 else:
576 data.__init__(self, readfile(filename, "user provided file-like object"), **kwargs)
579 class function(_dynamicdata):
581 defaultstyles = [style.line()]
583 def __init__(self, expression, title=notitle, min=None, max=None,
584 points=100, parser=mathtree.parser(), context={}):
586 if title is notitle:
587 self.title = expression
588 else:
589 self.title = title
590 self.min = min
591 self.max = max
592 self.numberofpoints = points
593 self.context = context.copy() # be save on late evaluations
594 self.yname, expression = [x.strip() for x in expression.split("=")]
595 self.mathtree = parser.parse(expression)
597 def getcolumnpointsindex_plotitem(self, plotitem, column):
598 return plotitem.points, plotitem.columns[column]
600 def initplotitem(self, plotitem, graph):
601 self.xname = None
602 for xname in self.mathtree.VarList():
603 if xname in graph.axes.keys():
604 if self.xname is None:
605 self.xname = xname
606 else:
607 raise ValueError("multiple variables found")
608 if self.xname is None:
609 raise ValueError("no variable found")
610 plotitem.columns = {self.xname: 0, self.yname: 1}
612 def getcolumnnames(self, plotitem):
613 return [self.xname, self.yname]
615 def adjustaxes(self, plotitem, graph, step):
616 if step == 0:
617 points = []
618 if self.min is not None:
619 points.append(self.min)
620 if self.max is not None:
621 points.append(self.max)
622 for privatedata, style in zip(plotitem.privatedatalist, plotitem.styles):
623 style.adjustaxis(privatedata, plotitem.sharedata, graph, self.xname, points, None)
624 elif step == 1:
625 xaxis = graph.axes[self.xname]
626 min, max = xaxis.getrange()
627 if self.min is not None: min = self.min
628 if self.max is not None: max = self.max
629 vmin = xaxis.convert(min)
630 vmax = xaxis.convert(max)
631 plotitem.points = []
632 for i in range(self.numberofpoints):
633 v = vmin + (vmax-vmin)*i / (self.numberofpoints-1.0)
634 x = xaxis.invert(v)
635 self.context[self.xname] = x
636 try:
637 y = self.mathtree.Calc(**self.context)
638 except (ArithmeticError, ValueError):
639 y = None
640 plotitem.points.append([x, y])
641 elif step == 2:
642 for privatedata, style in zip(plotitem.privatedatalist, plotitem.styles):
643 style.adjustaxis(privatedata, plotitem.sharedata, graph, self.yname, plotitem.points, 1)
646 class paramfunction(_staticdata):
648 defaultstyles = [style.line()]
650 def __init__(self, varname, min, max, expression, title=notitle, points=100, parser=mathtree.parser(), context={}):
651 if title is notitle:
652 self.title = expression
653 else:
654 self.title = title
655 varlist, expressionlist = expression.split("=")
656 keys = varlist.split(",")
657 mathtrees = parser.parse(expressionlist)
658 if len(keys) != len(mathtrees):
659 raise ValueError("unpack tuple of wrong size")
660 l = len(keys)
661 self.points = [None]*points
662 self.columns = {}
663 for index, key in enumerate(keys):
664 self.columns[key.strip()] = index
665 for i in range(points):
666 param = min + (max-min)*i / (points-1.0)
667 context[varname] = param
668 self.points[i] = [None]*l
669 for index, mathtree in enumerate(mathtrees):
670 self.points[i][index] = mathtree.Calc(**context)