[MERGE] Remove the embedded pychart library, and use the online version
[odoo/odoo.git] /
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com)
4
5 # Jockey is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2, or (at your option) any
8 # later version.
9 #
10 # Jockey is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
13 # for more details.
14 #
15 import pychart_util
16 import copy
17 import math
18
19 def _convert_item(v, typ, line):
20     if typ == "a":
21         try:
22             i = float(v)
23         except ValueError: # non-number
24             i = v
25         return i
26     elif typ == "d":
27         try:
28             return int(v)
29         except ValueError:
30             raise ValueError, "Can't convert %s to int; line=%s" % (v, line)
31     elif typ == "f":
32         try:
33             return float(v)
34         except ValueError:
35             raise ValueError, "Can't convert %s to float; line=%s" % (v, line)
36     elif typ == "s":
37         return v
38     else:
39         raise ValueError, "Unknown conversion type, type=%s; line=%s" % (typ,line)
40         
41 def parse_line(line, delim):
42     if delim.find("%") < 0:
43         return [ _convert_item(item, "a", None) for item in line.split(delim) ]
44     
45     data = []
46     idx = 0 # indexes delim
47     ch = 'f'
48     sep = ','
49
50     while idx < len(delim):
51         if delim[idx] != '%':
52             raise ValueError, "bad delimitor: '" + delim + "'"
53         ch = delim[idx+1]
54         idx += 2
55         sep = ""
56         while idx < len(delim) and delim[idx] != '%':
57             sep += delim[idx]
58             idx += 1
59         xx = line.split(sep, 1)
60         data.append(_convert_item(xx[0], ch, line))
61         if len(xx) >= 2:
62             line = xx[1]
63         else:
64             line = ""
65             break
66
67     if line != "":
68         for item in line.split(sep):
69             data.append(_convert_item(item, ch, line))
70     return data
71
72 def escape_string(str):
73     return str.replace("/", "//")
74
75 def extract_rows(data, *rows):
76     """Extract rows specified in the argument list.
77
78 >>> chart_data.extract_rows([[10,20], [30,40], [50,60]], 1, 2)
79 [[30,40],[50,60]]
80 """
81     try:
82         # for python 2.2
83         # return [data[r] for r in rows]
84         out = []
85         for r in rows:
86             out.append(data[r])
87         return out
88     except IndexError:
89         raise IndexError, "data=%s rows=%s" % (data, rows)
90     return out
91
92 def extract_columns(data, *cols):
93     """Extract columns specified in the argument list.
94
95 >>> chart_data.extract_columns([[10,20], [30,40], [50,60]], 0)
96 [[10],[30],[50]]
97 """
98     out = []
99     try:
100         # for python 2.2:
101         # return [ [r[c] for c in cols] for r in data]
102         for r in data:
103             col = []
104             for c in cols:
105                 col.append(r[c])
106             out.append(col)
107     except IndexError:
108         raise IndexError, "data=%s col=%s" % (data, col)        
109     return out
110
111             
112             
113
114 def moving_average(data, xcol, ycol, width):
115     """Compute the moving average of  YCOL'th column of each sample point
116 in  DATA. In particular, for each element  I in  DATA,
117 this function extracts up to  WIDTH*2+1 elements, consisting of
118  I itself,  WIDTH elements before  I, and  WIDTH
119 elements after  I. It then computes the mean of the  YCOL'th
120 column of these elements, and it composes a two-element sample
121 consisting of  XCOL'th element and the mean.
122
123 >>> data = [[10,20], [20,30], [30,50], [40,70], [50,5]]
124 ... chart_data.moving_average(data, 0, 1, 1)
125 [(10, 25.0), (20, 33.333333333333336), (30, 50.0), (40, 41.666666666666664), (50, 37.5)]
126
127   The above value actually represents:
128
129 [(10, (20+30)/2), (20, (20+30+50)/3), (30, (30+50+70)/3), 
130   (40, (50+70+5)/3), (50, (70+5)/2)]
131
132 """
133
134     
135     out = []
136     try:
137         for i in range(len(data)):
138             n = 0
139             total = 0
140             for j in range(i-width, i+width+1):
141                 if j >= 0 and j < len(data):
142                     total += data[j][ycol]
143                     n += 1
144             out.append((data[i][xcol], float(total) / n))
145     except IndexError:
146         raise IndexError, "bad data: %s,xcol=%d,ycol=%d,width=%d" % (data,xcol,ycol,width)
147     
148     return out
149     
150 def filter(func, data):
151     """Parameter <func> must be a single-argument
152     function that takes a sequence (i.e.,
153 a sample point) and returns a boolean. This procedure calls <func> on
154 each element in <data> and returns a list comprising elements for
155 which <func> returns True.
156
157 >>> data = [[1,5], [2,10], [3,13], [4,16]]
158 ... chart_data.filter(lambda x: x[1] % 2 == 0, data)
159 [[2,10], [4,16]].
160 """
161     
162     out = []
163     for r in data:
164         if func(r):
165             out.append(r)
166     return out
167
168 def transform(func, data):
169     """Apply <func> on each element in <data> and return the list
170 consisting of the return values from <func>.
171
172 >>> data = [[10,20], [30,40], [50,60]]
173 ... chart_data.transform(lambda x: [x[0], x[1]+1], data)
174 [[10, 21], [30, 41], [50, 61]]
175
176 """
177     out = []
178     for r in data:
179         out.append(func(r))
180     return out
181
182 def aggregate_rows(data, col):
183     out = copy.deepcopy(data)
184     total = 0
185     for r in out:
186         total += r[col]
187         r[col] = total
188     return out
189
190 def empty_line_p(s):
191     return s.strip() == ""
192
193 def fread_csv(fd, delim = ','):
194     """This function is similar to read_csv, except that it reads from
195     an open file handle <fd>, or any object that provides method "readline".
196
197 fd = open("foo", "r")
198 data = chart_data.fread_csv(fd, ",") """
199     
200     data = []
201     line = fd.readline()
202     while line != "":
203         if line[0] != '#' and not empty_line_p(line):
204             data.append(parse_line(line, delim))
205         line = fd.readline()
206     return data
207
208 def read_csv(path, delim = ','):
209     """This function reads
210     comma-separated values from file <path>. Empty lines and lines
211     beginning with "#" are ignored.  Parameter <delim> specifies how
212     a line is separated into values. If it does not contain the
213     letter "%", then <delim> marks the end of a value.
214     Otherwise, this function acts like scanf in C:
215
216 chart_data.read_csv("file", "%d,%s:%d")
217
218     Paramter <delim> currently supports
219     only three conversion format specifiers:
220     "d"(int), "f"(double), and "s"(string)."""
221         
222     f = open(path)
223     data = fread_csv(f, delim)
224     f.close()
225     return data
226
227 def fwrite_csv(fd, data):
228     """This function writes comma-separated <data> to <fd>. Parameter <fd> must be a file-like object
229     that supports the |write()| method."""
230     for v in data:
231         fd.write(",".join([str(x) for x in v]))
232         fd.write("\n")
233         
234 def write_csv(path, data):
235     """This function writes comma-separated values to <path>."""
236     fd = file(path, "w")
237     fwrite_csv(fd, data)
238     fd.close()
239     
240 def read_str(delim = ',', *lines):
241     """This function is similar to read_csv, but it reads data from the
242     list of <lines>.
243
244 fd = open("foo", "r")
245 data = chart_data.read_str(",", fd.readlines())"""
246
247     data = []
248     for line in lines:
249         com = parse_line(line, delim)
250         data.append(com)
251     return data
252     
253 def func(f, xmin, xmax, step = None):
254     """Create sample points from function <f>, which must be a
255     single-parameter function that returns a number (e.g., math.sin).
256     Parameters <xmin> and <xmax> specify the first and last X values, and
257     <step> specifies the sampling interval.
258
259 >>> chart_data.func(math.sin, 0, math.pi * 4, math.pi / 2)
260 [(0, 0.0), (1.5707963267948966, 1.0), (3.1415926535897931, 1.2246063538223773e-16), (4.7123889803846897, -1.0), (6.2831853071795862, -2.4492127076447545e-16), (7.8539816339744828, 1.0), (9.4247779607693793, 3.6738190614671318e-16), (10.995574287564276, -1.0)]
261
262 """
263     
264     data = []
265     x = xmin
266     if not step:
267         step = (xmax - xmin) / 100.0
268     while x < xmax:
269         data.append((x, f(x)))
270         x += step
271     return data
272
273 def _nr_data(data, col):
274     nr_data = 0
275     for d in data:
276         nr_data += d[col]
277     return nr_data
278     
279 def median(data, freq_col=1):
280     """Compute the median of the <freq_col>'th column of the values is <data>.
281
282 >>> chart_data.median([(10,20), (20,4), (30,5)], 0)
283 20
284 >>> chart_data.median([(10,20), (20,4), (30,5)], 1)
285 5.
286     """
287     
288     nr_data = _nr_data(data, freq_col)
289     median_idx = nr_data / 2
290     i = 0
291     for d in data:
292         i += d[freq_col]
293         if i >= median_idx:
294             return d
295     raise Exception, "??? median ???"
296
297 def cut_extremes(data, cutoff_percentage, freq_col=1):
298     nr_data = _nr_data(data, freq_col)
299     min_idx = nr_data * cutoff_percentage / 100.0
300     max_idx = nr_data * (100 - cutoff_percentage) / 100.0
301     r = []
302     
303     i = 0
304     for d in data:
305         if i < min_idx:
306             if i + d[freq_col] >= min_idx:
307                 x = copy.deepcopy(d)
308                 x[freq_col] = x[freq_col] - (min_idx - i)
309                 r.append(x)
310             i += d[freq_col]
311             continue
312         elif i + d[freq_col] >= max_idx:
313             if i < max_idx and i + d[freq_col] >= max_idx:
314                 x = copy.deepcopy(d)
315                 x[freq_col] = x[freq_col] - (max_idx - i)
316                 r.append(x)
317             break
318         i += d[freq_col]
319         r.append(d)
320     return r
321
322 def mean(data, val_col, freq_col):
323     nr_data = 0
324     sum = 0
325     for d in data:
326         sum += d[val_col] * d[freq_col]
327         nr_data += d[freq_col]
328     if nr_data == 0:
329         raise IndexError, "data is empty"
330
331     return sum / float(nr_data)
332
333 def mean_samples(data, xcol, ycollist):
334     """Create a sample list that contains
335     the mean of the original list.
336
337 >>> chart_data.mean_samples([ [1, 10, 15], [2, 5, 10], [3, 8, 33] ], 0, (1, 2))
338 [(1, 12.5), (2, 7.5), (3, 20.5)]
339 """
340     out = []
341     numcol = len(ycollist)
342     try:
343         for elem in data:
344             v = 0
345             for col in ycollist:
346                 v += elem[col]
347             out.append( (elem[xcol], float(v) / numcol) )
348     except IndexError:
349         raise IndexError, "bad data: %s,xcol=%d,ycollist=%s" % (data,xcol,ycollist)
350     
351     return out
352
353 def stddev_samples(data, xcol, ycollist, delta = 1.0):
354     """Create a sample list that contains the mean and standard deviation of the original list. Each element in the returned list contains following values: [MEAN, STDDEV, MEAN - STDDEV*delta, MEAN + STDDEV*delta].
355
356 >>> chart_data.stddev_samples([ [1, 10, 15, 12, 15], [2, 5, 10, 5, 10], [3, 32, 33, 35, 36], [4,16,66, 67, 68] ], 0, range(1,5))
357 [(1, 13.0, 2.1213203435596424, 10.878679656440358, 15.121320343559642), (2, 7.5, 2.5, 5.0, 10.0), (3, 34.0, 1.5811388300841898, 32.418861169915807, 35.581138830084193), (4, 54.25, 22.094965489902897, 32.155034510097103, 76.344965489902904)]
358 """
359     out = []
360     numcol = len(ycollist)
361     try:
362         for elem in data:
363             total = 0
364             for col in ycollist:
365                 total += elem[col]
366             mean = float(total) / numcol
367             variance = 0
368             for col in ycollist:
369                 variance += (mean - elem[col]) ** 2
370             stddev = math.sqrt(variance / numcol) * delta
371             out.append( (elem[xcol], mean, stddev, mean-stddev, mean+stddev) )
372             
373             
374             
375     except IndexError:
376         raise IndexError, "bad data: %s,xcol=%d,ycollist=%s" % (data,xcol,ycollist)
377     return out
378
379 def nearest_match(data, col, val):
380     min_delta = None
381     match = None
382     
383     for d in data:
384         if min_delta == None or abs(d[col] - val) < min_delta:
385             min_delta = abs(d[col] - val)
386             match = d
387     pychart_util.warn("XXX ", match)
388     return match