Commit | Line | Data |
---|---|---|
24a3136a DS |
1 | #!/usr/bin/env python |
2 | # -*- coding: utf-8 -*- | |
3 | ||
4 | # Serie.py | |
5 | # | |
6 | # Copyright (c) 2008 Magnun Leno da Silva | |
7 | # | |
8 | # Author: Magnun Leno da Silva <magnun.leno@gmail.com> | |
9 | # | |
10 | # This program is free software; you can redistribute it and/or | |
11 | # modify it under the terms of the GNU Lesser General Public License | |
12 | # as published by the Free Software Foundation; either version 2 of | |
13 | # the License, or (at your option) any later version. | |
14 | # | |
15 | # This program is distributed in the hope that it will be useful, | |
16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
18 | # GNU General Public License for more details. | |
19 | # | |
20 | # You should have received a copy of the GNU Lesser General Public | |
21 | # License along with this program; if not, write to the Free Software | |
22 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 | |
23 | # USA | |
24 | ||
25 | # Contributor: Rodrigo Moreiro Araujo <alf.rodrigo@gmail.com> | |
26 | ||
27 | #import cairoplot | |
28 | import doctest | |
29 | ||
30 | NUMTYPES = (int, float, long) | |
31 | LISTTYPES = (list, tuple) | |
32 | STRTYPES = (str, unicode) | |
33 | FILLING_TYPES = ['linear', 'solid', 'gradient'] | |
34 | DEFAULT_COLOR_FILLING = 'solid' | |
35 | #TODO: Define default color list | |
36 | DEFAULT_COLOR_LIST = None | |
37 | ||
38 | class Data(object): | |
39 | ''' | |
40 | Class that models the main data structure. | |
41 | It can hold: | |
42 | - a number type (int, float or long) | |
43 | - a tuple, witch represents a point and can have 2 or 3 items (x,y,z) | |
44 | - if a list is passed it will be converted to a tuple. | |
45 | ||
46 | obs: In case a tuple is passed it will convert to tuple | |
47 | ''' | |
48 | def __init__(self, data=None, name=None, parent=None): | |
49 | ''' | |
50 | Starts main atributes from the Data class | |
51 | @name - Name for each point; | |
52 | @content - The real data, can be an int, float, long or tuple, which | |
53 | represents a point (x,y) or (x,y,z); | |
54 | @parent - A pointer that give the data access to it's parent. | |
55 | ||
56 | Usage: | |
57 | >>> d = Data(name='empty'); print d | |
58 | empty: () | |
59 | >>> d = Data((1,1),'point a'); print d | |
60 | point a: (1, 1) | |
61 | >>> d = Data((1,2,3),'point b'); print d | |
62 | point b: (1, 2, 3) | |
63 | >>> d = Data([2,3],'point c'); print d | |
64 | point c: (2, 3) | |
65 | >>> d = Data(12, 'simple value'); print d | |
66 | simple value: 12 | |
67 | ''' | |
68 | # Initial values | |
69 | self.__content = None | |
70 | self.__name = None | |
71 | ||
72 | # Setting passed values | |
73 | self.parent = parent | |
74 | self.name = name | |
75 | self.content = data | |
76 | ||
77 | # Name property | |
78 | @apply | |
79 | def name(): | |
80 | doc = ''' | |
81 | Name is a read/write property that controls the input of name. | |
82 | - If passed an invalid value it cleans the name with None | |
83 | ||
84 | Usage: | |
85 | >>> d = Data(13); d.name = 'name_test'; print d | |
86 | name_test: 13 | |
87 | >>> d.name = 11; print d | |
88 | 13 | |
89 | >>> d.name = 'other_name'; print d | |
90 | other_name: 13 | |
91 | >>> d.name = None; print d | |
92 | 13 | |
93 | >>> d.name = 'last_name'; print d | |
94 | last_name: 13 | |
95 | >>> d.name = ''; print d | |
96 | 13 | |
97 | ''' | |
98 | def fget(self): | |
99 | ''' | |
100 | returns the name as a string | |
101 | ''' | |
102 | return self.__name | |
103 | ||
104 | def fset(self, name): | |
105 | ''' | |
106 | Sets the name of the Data | |
107 | ''' | |
108 | if type(name) in STRTYPES and len(name) > 0: | |
109 | self.__name = name | |
110 | else: | |
111 | self.__name = None | |
112 | ||
113 | ||
114 | ||
115 | return property(**locals()) | |
116 | ||
117 | # Content property | |
118 | @apply | |
119 | def content(): | |
120 | doc = ''' | |
121 | Content is a read/write property that validate the data passed | |
122 | and return it. | |
123 | ||
124 | Usage: | |
125 | >>> d = Data(); d.content = 13; d.content | |
126 | 13 | |
127 | >>> d = Data(); d.content = (1,2); d.content | |
128 | (1, 2) | |
129 | >>> d = Data(); d.content = (1,2,3); d.content | |
130 | (1, 2, 3) | |
131 | >>> d = Data(); d.content = [1,2,3]; d.content | |
132 | (1, 2, 3) | |
133 | >>> d = Data(); d.content = [1.5,.2,3.3]; d.content | |
134 | (1.5, 0.20000000000000001, 3.2999999999999998) | |
135 | ''' | |
136 | def fget(self): | |
137 | ''' | |
138 | Return the content of Data | |
139 | ''' | |
140 | return self.__content | |
141 | ||
142 | def fset(self, data): | |
143 | ''' | |
144 | Ensures that data is a valid tuple/list or a number (int, float | |
145 | or long) | |
146 | ''' | |
147 | # Type: None | |
148 | if data is None: | |
149 | self.__content = None | |
150 | return | |
151 | ||
152 | # Type: Int or Float | |
153 | elif type(data) in NUMTYPES: | |
154 | self.__content = data | |
155 | ||
156 | # Type: List or Tuple | |
157 | elif type(data) in LISTTYPES: | |
158 | # Ensures the correct size | |
159 | if len(data) not in (2, 3): | |
160 | raise TypeError, "Data (as list/tuple) must have 2 or 3 items" | |
161 | return | |
162 | ||
163 | # Ensures that all items in list/tuple is a number | |
164 | isnum = lambda x : type(x) not in NUMTYPES | |
165 | ||
166 | if max(map(isnum, data)): | |
167 | # An item in data isn't an int or a float | |
168 | raise TypeError, "All content of data must be a number (int or float)" | |
169 | ||
170 | # Convert the tuple to list | |
171 | if type(data) is list: | |
172 | data = tuple(data) | |
173 | ||
174 | # Append a copy and sets the type | |
175 | self.__content = data[:] | |
176 | ||
177 | # Unknown type! | |
178 | else: | |
179 | self.__content = None | |
180 | raise TypeError, "Data must be an int, float or a tuple with two or three items" | |
181 | return | |
182 | ||
183 | return property(**locals()) | |
184 | ||
185 | ||
186 | def clear(self): | |
187 | ''' | |
188 | Clear the all Data (content, name and parent) | |
189 | ''' | |
190 | self.content = None | |
191 | self.name = None | |
192 | self.parent = None | |
193 | ||
194 | def copy(self): | |
195 | ''' | |
196 | Returns a copy of the Data structure | |
197 | ''' | |
198 | # The copy | |
199 | new_data = Data() | |
200 | if self.content is not None: | |
201 | # If content is a point | |
202 | if type(self.content) is tuple: | |
203 | new_data.__content = self.content[:] | |
204 | ||
205 | # If content is a number | |
206 | else: | |
207 | new_data.__content = self.content | |
208 | ||
209 | # If it has a name | |
210 | if self.name is not None: | |
211 | new_data.__name = self.name | |
212 | ||
213 | return new_data | |
214 | ||
215 | def __str__(self): | |
216 | ''' | |
217 | Return a string representation of the Data structure | |
218 | ''' | |
219 | if self.name is None: | |
220 | if self.content is None: | |
221 | return '' | |
222 | return str(self.content) | |
223 | else: | |
224 | if self.content is None: | |
225 | return self.name+": ()" | |
226 | return self.name+": "+str(self.content) | |
227 | ||
228 | def __len__(self): | |
229 | ''' | |
230 | Return the length of the Data. | |
231 | - If it's a number return 1; | |
232 | - If it's a list return it's length; | |
233 | - If its None return 0. | |
234 | ''' | |
235 | if self.content is None: | |
236 | return 0 | |
237 | elif type(self.content) in NUMTYPES: | |
238 | return 1 | |
239 | return len(self.content) | |
240 | ||
241 | ||
242 | ||
243 | ||
244 | class Group(object): | |
245 | ''' | |
246 | Class that models a group of data. Every value (int, float, long, tuple | |
247 | or list) passed is converted to a list of Data. | |
248 | It can receive: | |
249 | - A single number (int, float, long); | |
250 | - A list of numbers; | |
251 | - A tuple of numbers; | |
252 | - An instance of Data; | |
253 | - A list of Data; | |
254 | ||
255 | Obs: If a tuple with 2 or 3 items is passed it is converted to a point. | |
256 | If a tuple with only 1 item is passed it's converted to a number; | |
257 | If a tuple with more than 2 items is passed it's converted to a | |
258 | list of numbers | |
259 | ''' | |
260 | def __init__(self, group=None, name=None, parent=None): | |
261 | ''' | |
262 | Starts main atributes in Group instance. | |
263 | @data_list - a list of data which forms the group; | |
264 | @range - a range that represent the x axis of possible functions; | |
265 | @name - name of the data group; | |
266 | @parent - the Serie parent of this group. | |
267 | ||
268 | Usage: | |
269 | >>> g = Group(13, 'simple number'); print g | |
270 | simple number ['13'] | |
271 | >>> g = Group((1,2), 'simple point'); print g | |
272 | simple point ['(1, 2)'] | |
273 | >>> g = Group([1,2,3,4], 'list of numbers'); print g | |
274 | list of numbers ['1', '2', '3', '4'] | |
275 | >>> g = Group((1,2,3,4),'int in tuple'); print g | |
276 | int in tuple ['1', '2', '3', '4'] | |
277 | >>> g = Group([(1,2),(2,3),(3,4)], 'list of points'); print g | |
278 | list of points ['(1, 2)', '(2, 3)', '(3, 4)'] | |
279 | >>> g = Group([[1,2,3],[1,2,3]], '2D coordinate lists'); print g | |
280 | 2D coordinated lists ['(1, 1)', '(2, 2)', '(3, 3)'] | |
281 | >>> g = Group([[1,2],[1,2],[1,2]], '3D coordinate lists'); print g | |
282 | 3D coordinated lists ['(1, 1, 1)', '(2, 2, 2)'] | |
283 | ''' | |
284 | # Initial values | |
285 | self.__data_list = [] | |
286 | self.__range = [] | |
287 | self.__name = None | |
288 | ||
289 | ||
290 | self.parent = parent | |
291 | self.name = name | |
292 | self.data_list = group | |
293 | ||
294 | # Name property | |
295 | @apply | |
296 | def name(): | |
297 | doc = ''' | |
298 | Name is a read/write property that controls the input of name. | |
299 | - If passed an invalid value it cleans the name with None | |
300 | ||
301 | Usage: | |
302 | >>> g = Group(13); g.name = 'name_test'; print g | |
303 | name_test ['13'] | |
304 | >>> g.name = 11; print g | |
305 | ['13'] | |
306 | >>> g.name = 'other_name'; print g | |
307 | other_name ['13'] | |
308 | >>> g.name = None; print g | |
309 | ['13'] | |
310 | >>> g.name = 'last_name'; print g | |
311 | last_name ['13'] | |
312 | >>> g.name = ''; print g | |
313 | ['13'] | |
314 | ''' | |
315 | def fget(self): | |
316 | ''' | |
317 | Returns the name as a string | |
318 | ''' | |
319 | return self.__name | |
320 | ||
321 | def fset(self, name): | |
322 | ''' | |
323 | Sets the name of the Group | |
324 | ''' | |
325 | if type(name) in STRTYPES and len(name) > 0: | |
326 | self.__name = name | |
327 | else: | |
328 | self.__name = None | |
329 | ||
330 | return property(**locals()) | |
331 | ||
332 | # data_list property | |
333 | @apply | |
334 | def data_list(): | |
335 | doc = ''' | |
336 | The data_list is a read/write property that can be a list of | |
337 | numbers, a list of points or a list of 2 or 3 coordinate lists. This | |
338 | property uses mainly the self.add_data method. | |
339 | ||
340 | Usage: | |
341 | >>> g = Group(); g.data_list = 13; print g | |
342 | ['13'] | |
343 | >>> g.data_list = (1,2); print g | |
344 | ['(1, 2)'] | |
345 | >>> g.data_list = Data((1,2),'point a'); print g | |
346 | ['point a: (1, 2)'] | |
347 | >>> g.data_list = [1,2,3]; print g | |
348 | ['1', '2', '3'] | |
349 | >>> g.data_list = (1,2,3,4); print g | |
350 | ['1', '2', '3', '4'] | |
351 | >>> g.data_list = [(1,2),(2,3),(3,4)]; print g | |
352 | ['(1, 2)', '(2, 3)', '(3, 4)'] | |
353 | >>> g.data_list = [[1,2],[1,2]]; print g | |
354 | ['(1, 1)', '(2, 2)'] | |
355 | >>> g.data_list = [[1,2],[1,2],[1,2]]; print g | |
356 | ['(1, 1, 1)', '(2, 2, 2)'] | |
357 | >>> g.range = (10); g.data_list = lambda x:x**2; print g | |
358 | ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)'] | |
359 | ''' | |
360 | def fget(self): | |
361 | ''' | |
362 | Returns the value of data_list | |
363 | ''' | |
364 | return self.__data_list | |
365 | ||
366 | def fset(self, group): | |
367 | ''' | |
368 | Ensures that group is valid. | |
369 | ''' | |
370 | # None | |
371 | if group is None: | |
372 | self.__data_list = [] | |
373 | ||
374 | # Int/float/long or Instance of Data | |
375 | elif type(group) in NUMTYPES or isinstance(group, Data): | |
376 | # Clean data_list | |
377 | self.__data_list = [] | |
378 | self.add_data(group) | |
379 | ||
380 | # One point | |
381 | elif type(group) is tuple and len(group) in (2,3): | |
382 | self.__data_list = [] | |
383 | self.add_data(group) | |
384 | ||
385 | # list of items | |
386 | elif type(group) in LISTTYPES and type(group[0]) is not list: | |
387 | # Clean data_list | |
388 | self.__data_list = [] | |
389 | for item in group: | |
390 | # try to append and catch an exception | |
391 | self.add_data(item) | |
392 | ||
393 | # function lambda | |
394 | elif callable(group): | |
395 | # Explicit is better than implicit | |
396 | function = group | |
397 | # Has range | |
398 | if len(self.range) is not 0: | |
399 | # Clean data_list | |
400 | self.__data_list = [] | |
401 | # Generate values for the lambda function | |
402 | for x in self.range: | |
403 | #self.add_data((x,round(group(x),2))) | |
404 | self.add_data((x,function(x))) | |
405 | ||
406 | # Only have range in parent | |
407 | elif self.parent is not None and len(self.parent.range) is not 0: | |
408 | # Copy parent range | |
409 | self.__range = self.parent.range[:] | |
410 | # Clean data_list | |
411 | self.__data_list = [] | |
412 | # Generate values for the lambda function | |
413 | for x in self.range: | |
414 | #self.add_data((x,round(group(x),2))) | |
415 | self.add_data((x,function(x))) | |
416 | ||
417 | # Don't have range anywhere | |
418 | else: | |
419 | # x_data don't exist | |
420 | raise Exception, "Data argument is valid but to use function type please set x_range first" | |
421 | ||
422 | # Coordinate Lists | |
423 | elif type(group) in LISTTYPES and type(group[0]) is list: | |
424 | # Clean data_list | |
425 | self.__data_list = [] | |
426 | data = [] | |
427 | if len(group) == 3: | |
428 | data = zip(group[0], group[1], group[2]) | |
429 | elif len(group) == 2: | |
430 | data = zip(group[0], group[1]) | |
431 | else: | |
432 | raise TypeError, "Only one list of coordinates was received." | |
433 | ||
434 | for item in data: | |
435 | self.add_data(item) | |
436 | ||
437 | else: | |
438 | raise TypeError, "Group type not supported" | |
439 | ||
440 | return property(**locals()) | |
441 | ||
442 | @apply | |
443 | def range(): | |
444 | doc = ''' | |
445 | The range is a read/write property that generates a range of values | |
446 | for the x axis of the functions. When passed a tuple it almost works | |
447 | like the built-in range funtion: | |
448 | - 1 item, represent the end of the range started from 0; | |
449 | - 2 items, represents the start and the end, respectively; | |
450 | - 3 items, the last one represents the step; | |
451 | ||
452 | When passed a list the range function understands as a valid range. | |
453 | ||
454 | Usage: | |
455 | >>> g = Group(); g.range = 10; print g.range | |
456 | [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] | |
457 | >>> g = Group(); g.range = (5); print g.range | |
458 | [0.0, 1.0, 2.0, 3.0, 4.0] | |
459 | >>> g = Group(); g.range = (1,7); print g.range | |
460 | [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] | |
461 | >>> g = Group(); g.range = (0,10,2); print g.range | |
462 | [0.0, 2.0, 4.0, 6.0, 8.0] | |
463 | >>> | |
464 | >>> g = Group(); g.range = [0]; print g.range | |
465 | [0.0] | |
466 | >>> g = Group(); g.range = [0,10,20]; print g.range | |
467 | [0.0, 10.0, 20.0] | |
468 | ''' | |
469 | def fget(self): | |
470 | ''' | |
471 | Returns the range | |
472 | ''' | |
473 | return self.__range | |
474 | ||
475 | def fset(self, x_range): | |
476 | ''' | |
477 | Controls the input of a valid type and generate the range | |
478 | ''' | |
479 | # if passed a simple number convert to tuple | |
480 | if type(x_range) in NUMTYPES: | |
481 | x_range = (x_range,) | |
482 | ||
483 | # A list, just convert to float | |
484 | if type(x_range) is list and len(x_range) > 0: | |
485 | # Convert all to float | |
486 | x_range = map(float, x_range) | |
487 | # Prevents repeated values and convert back to list | |
488 | self.__range = list(set(x_range[:])) | |
489 | # Sort the list to ascending order | |
490 | self.__range.sort() | |
491 | ||
492 | # A tuple, must check the lengths and generate the values | |
493 | elif type(x_range) is tuple and len(x_range) in (1,2,3): | |
494 | # Convert all to float | |
495 | x_range = map(float, x_range) | |
496 | ||
497 | # Inital values | |
498 | start = 0.0 | |
499 | step = 1.0 | |
500 | end = 0.0 | |
501 | ||
502 | # Only the end and it can't be less or iqual to 0 | |
503 | if len(x_range) is 1 and x_range > 0: | |
504 | end = x_range[0] | |
505 | ||
506 | # The start and the end but the start must be less then the end | |
507 | elif len(x_range) is 2 and x_range[0] < x_range[1]: | |
508 | start = x_range[0] | |
509 | end = x_range[1] | |
510 | ||
511 | # All 3, but the start must be less then the end | |
512 | elif x_range[0] <= x_range[1]: | |
513 | start = x_range[0] | |
514 | end = x_range[1] | |
515 | step = x_range[2] | |
516 | ||
517 | # Starts the range | |
518 | self.__range = [] | |
519 | # Generate the range | |
520 | # Can't use the range function because it doesn't support float values | |
521 | while start < end: | |
522 | self.__range.append(start) | |
523 | start += step | |
524 | ||
525 | # Incorrect type | |
526 | else: | |
527 | raise Exception, "x_range must be a list with one or more items or a tuple with 2 or 3 items" | |
528 | ||
529 | return property(**locals()) | |
530 | ||
531 | def add_data(self, data, name=None): | |
532 | ''' | |
533 | Append a new data to the data_list. | |
534 | - If data is an instance of Data, append it | |
535 | - If it's an int, float, tuple or list create an instance of Data and append it | |
536 | ||
537 | Usage: | |
538 | >>> g = Group() | |
539 | >>> g.add_data(12); print g | |
540 | ['12'] | |
541 | >>> g.add_data(7,'other'); print g | |
542 | ['12', 'other: 7'] | |
543 | >>> | |
544 | >>> g = Group() | |
545 | >>> g.add_data((1,1),'a'); print g | |
546 | ['a: (1, 1)'] | |
547 | >>> g.add_data((2,2),'b'); print g | |
548 | ['a: (1, 1)', 'b: (2, 2)'] | |
549 | >>> | |
550 | >>> g.add_data(Data((1,2),'c')); print g | |
551 | ['a: (1, 1)', 'b: (2, 2)', 'c: (1, 2)'] | |
552 | ''' | |
553 | if not isinstance(data, Data): | |
554 | # Try to convert | |
555 | data = Data(data,name,self) | |
556 | ||
557 | if data.content is not None: | |
558 | self.__data_list.append(data.copy()) | |
559 | self.__data_list[-1].parent = self | |
560 | ||
561 | ||
562 | def to_list(self): | |
563 | ''' | |
564 | Returns the group as a list of numbers (int, float or long) or a | |
565 | list of tuples (points 2D or 3D). | |
566 | ||
567 | Usage: | |
568 | >>> g = Group([1,2,3,4],'g1'); g.to_list() | |
569 | [1, 2, 3, 4] | |
570 | >>> g = Group([(1,2),(2,3),(3,4)],'g2'); g.to_list() | |
571 | [(1, 2), (2, 3), (3, 4)] | |
572 | >>> g = Group([(1,2,3),(3,4,5)],'g2'); g.to_list() | |
573 | [(1, 2, 3), (3, 4, 5)] | |
574 | ''' | |
575 | return [data.content for data in self] | |
576 | ||
577 | def copy(self): | |
578 | ''' | |
579 | Returns a copy of this group | |
580 | ''' | |
581 | new_group = Group() | |
582 | new_group.__name = self.__name | |
583 | if self.__range is not None: | |
584 | new_group.__range = self.__range[:] | |
585 | for data in self: | |
586 | new_group.add_data(data.copy()) | |
587 | return new_group | |
588 | ||
589 | def get_names(self): | |
590 | ''' | |
591 | Return a list with the names of all data in this group | |
592 | ''' | |
593 | names = [] | |
594 | for data in self: | |
595 | if data.name is None: | |
596 | names.append('Data '+str(data.index()+1)) | |
597 | else: | |
598 | names.append(data.name) | |
599 | return names | |
600 | ||
601 | ||
602 | def __str__ (self): | |
603 | ''' | |
604 | Returns a string representing the Group | |
605 | ''' | |
606 | ret = "" | |
607 | if self.name is not None: | |
608 | ret += self.name + " " | |
609 | if len(self) > 0: | |
610 | list_str = [str(item) for item in self] | |
611 | ret += str(list_str) | |
612 | else: | |
613 | ret += "[]" | |
614 | return ret | |
615 | ||
616 | def __getitem__(self, key): | |
617 | ''' | |
618 | Makes a Group iterable, based in the data_list property | |
619 | ''' | |
620 | return self.data_list[key] | |
621 | ||
622 | def __len__(self): | |
623 | ''' | |
624 | Returns the length of the Group, based in the data_list property | |
625 | ''' | |
626 | return len(self.data_list) | |
627 | ||
628 | ||
629 | class Colors(object): | |
630 | ''' | |
631 | Class that models the colors its labels (names) and its properties, RGB | |
632 | and filling type. | |
633 | ||
634 | It can receive: | |
635 | - A list where each item is a list with 3 or 4 items. The | |
636 | first 3 items represent the RGB values and the last argument | |
637 | defines the filling type. The list will be converted to a dict | |
638 | and each color will receve a name based in its position in the | |
639 | list. | |
640 | - A dictionary where each key will be the color name and its item | |
641 | can be a list with 3 or 4 items. The first 3 items represent | |
642 | the RGB colors and the last argument defines the filling type. | |
643 | ''' | |
644 | def __init__(self, color_list=None): | |
645 | ''' | |
646 | Start the color_list property | |
647 | @ color_list - the list or dict contaning the colors properties. | |
648 | ''' | |
649 | self.__color_list = None | |
650 | ||
651 | self.color_list = color_list | |
652 | ||
653 | @apply | |
654 | def color_list(): | |
655 | doc = ''' | |
656 | >>> c = Colors([[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']]) | |
657 | >>> print c.color_list | |
658 | {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']} | |
659 | >>> c.color_list = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')] | |
660 | >>> print c.color_list | |
661 | {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']} | |
662 | >>> c.color_list = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)} | |
663 | >>> print c.color_list | |
664 | {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']} | |
665 | ''' | |
666 | def fget(self): | |
667 | ''' | |
668 | Return the color list | |
669 | ''' | |
670 | return self.__color_list | |
671 | ||
672 | def fset(self, color_list): | |
673 | ''' | |
674 | Format the color list to a dictionary | |
675 | ''' | |
676 | if color_list is None: | |
677 | self.__color_list = None | |
678 | return | |
679 | ||
680 | if type(color_list) in LISTTYPES and type(color_list[0]) in LISTTYPES: | |
681 | old_color_list = color_list[:] | |
682 | color_list = {} | |
683 | for index, color in enumerate(old_color_list): | |
684 | if len(color) is 3 and max(map(type, color)) in NUMTYPES: | |
685 | color_list['Color '+str(index+1)] = list(color)+[DEFAULT_COLOR_FILLING] | |
686 | elif len(color) is 4 and max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES: | |
687 | color_list['Color '+str(index+1)] = list(color) | |
688 | else: | |
689 | raise TypeError, "Unsuported color format" | |
690 | elif type(color_list) is not dict: | |
691 | raise TypeError, "Unsuported color format" | |
692 | ||
693 | for name, color in color_list.items(): | |
694 | if len(color) is 3: | |
695 | if max(map(type, color)) in NUMTYPES: | |
696 | color_list[name] = list(color)+[DEFAULT_COLOR_FILLING] | |
697 | else: | |
698 | raise TypeError, "Unsuported color format" | |
699 | elif len(color) is 4: | |
700 | if max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES: | |
701 | color_list[name] = list(color) | |
702 | else: | |
703 | raise TypeError, "Unsuported color format" | |
704 | self.__color_list = color_list.copy() | |
705 | ||
706 | return property(**locals()) | |
707 | ||
708 | ||
709 | class Series(object): | |
710 | ''' | |
711 | Class that models a Series (group of groups). Every value (int, float, | |
712 | long, tuple or list) passed is converted to a list of Group or Data. | |
713 | It can receive: | |
714 | - a single number or point, will be converted to a Group of one Data; | |
715 | - a list of numbers, will be converted to a group of numbers; | |
716 | - a list of tuples, will converted to a single Group of points; | |
717 | - a list of lists of numbers, each 'sublist' will be converted to a | |
718 | group of numbers; | |
719 | - a list of lists of tuples, each 'sublist' will be converted to a | |
720 | group of points; | |
721 | - a list of lists of lists, the content of the 'sublist' will be | |
722 | processed as coordinated lists and the result will be converted to | |
723 | a group of points; | |
724 | - a Dictionary where each item can be the same of the list: number, | |
725 | point, list of numbers, list of points or list of lists (coordinated | |
726 | lists); | |
727 | - an instance of Data; | |
728 | - an instance of group. | |
729 | ''' | |
730 | def __init__(self, series=None, name=None, property=[], colors=None): | |
731 | ''' | |
732 | Starts main atributes in Group instance. | |
733 | @series - a list, dict of data of which the series is composed; | |
734 | @name - name of the series; | |
735 | @property - a list/dict of properties to be used in the plots of | |
736 | this Series | |
737 | ||
738 | Usage: | |
739 | >>> print Series([1,2,3,4]) | |
740 | ["Group 1 ['1', '2', '3', '4']"] | |
741 | >>> print Series([[1,2,3],[4,5,6]]) | |
742 | ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"] | |
743 | >>> print Series((1,2)) | |
744 | ["Group 1 ['(1, 2)']"] | |
745 | >>> print Series([(1,2),(2,3)]) | |
746 | ["Group 1 ['(1, 2)', '(2, 3)']"] | |
747 | >>> print Series([[(1,2),(2,3)],[(4,5),(5,6)]]) | |
748 | ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"] | |
749 | >>> print Series([[[1,2,3],[1,2,3],[1,2,3]]]) | |
750 | ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"] | |
751 | >>> print Series({'g1':[1,2,3], 'g2':[4,5,6]}) | |
752 | ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"] | |
753 | >>> print Series({'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]}) | |
754 | ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"] | |
755 | >>> print Series({'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]}) | |
756 | ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"] | |
757 | >>> print Series(Data(1,'d1')) | |
758 | ["Group 1 ['d1: 1']"] | |
759 | >>> print Series(Group([(1,2),(2,3)],'g1')) | |
760 | ["g1 ['(1, 2)', '(2, 3)']"] | |
761 | ''' | |
762 | # Intial values | |
763 | self.__group_list = [] | |
764 | self.__name = None | |
765 | self.__range = None | |
766 | ||
767 | # TODO: Implement colors with filling | |
768 | self.__colors = None | |
769 | ||
770 | self.name = name | |
771 | self.group_list = series | |
772 | self.colors = colors | |
773 | ||
774 | # Name property | |
775 | @apply | |
776 | def name(): | |
777 | doc = ''' | |
778 | Name is a read/write property that controls the input of name. | |
779 | - If passed an invalid value it cleans the name with None | |
780 | ||
781 | Usage: | |
782 | >>> s = Series(13); s.name = 'name_test'; print s | |
783 | name_test ["Group 1 ['13']"] | |
784 | >>> s.name = 11; print s | |
785 | ["Group 1 ['13']"] | |
786 | >>> s.name = 'other_name'; print s | |
787 | other_name ["Group 1 ['13']"] | |
788 | >>> s.name = None; print s | |
789 | ["Group 1 ['13']"] | |
790 | >>> s.name = 'last_name'; print s | |
791 | last_name ["Group 1 ['13']"] | |
792 | >>> s.name = ''; print s | |
793 | ["Group 1 ['13']"] | |
794 | ''' | |
795 | def fget(self): | |
796 | ''' | |
797 | Returns the name as a string | |
798 | ''' | |
799 | return self.__name | |
800 | ||
801 | def fset(self, name): | |
802 | ''' | |
803 | Sets the name of the Group | |
804 | ''' | |
805 | if type(name) in STRTYPES and len(name) > 0: | |
806 | self.__name = name | |
807 | else: | |
808 | self.__name = None | |
809 | ||
810 | return property(**locals()) | |
811 | ||
812 | ||
813 | ||
814 | # Colors property | |
815 | @apply | |
816 | def colors(): | |
817 | doc = ''' | |
818 | >>> s = Series() | |
819 | >>> s.colors = [[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']] | |
820 | >>> print s.colors | |
821 | {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']} | |
822 | >>> s.colors = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')] | |
823 | >>> print s.colors | |
824 | {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']} | |
825 | >>> s.colors = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)} | |
826 | >>> print s.colors | |
827 | {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']} | |
828 | ''' | |
829 | def fget(self): | |
830 | ''' | |
831 | Return the color list | |
832 | ''' | |
833 | return self.__colors.color_list | |
834 | ||
835 | def fset(self, colors): | |
836 | ''' | |
837 | Format the color list to a dictionary | |
838 | ''' | |
839 | self.__colors = Colors(colors) | |
840 | ||
841 | return property(**locals()) | |
842 | ||
843 | @apply | |
844 | def range(): | |
845 | doc = ''' | |
846 | The range is a read/write property that generates a range of values | |
847 | for the x axis of the functions. When passed a tuple it almost works | |
848 | like the built-in range funtion: | |
849 | - 1 item, represent the end of the range started from 0; | |
850 | - 2 items, represents the start and the end, respectively; | |
851 | - 3 items, the last one represents the step; | |
852 | ||
853 | When passed a list the range function understands as a valid range. | |
854 | ||
855 | Usage: | |
856 | >>> s = Series(); s.range = 10; print s.range | |
857 | [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] | |
858 | >>> s = Series(); s.range = (5); print s.range | |
859 | [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] | |
860 | >>> s = Series(); s.range = (1,7); print s.range | |
861 | [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] | |
862 | >>> s = Series(); s.range = (0,10,2); print s.range | |
863 | [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] | |
864 | >>> | |
865 | >>> s = Series(); s.range = [0]; print s.range | |
866 | [0.0] | |
867 | >>> s = Series(); s.range = [0,10,20]; print s.range | |
868 | [0.0, 10.0, 20.0] | |
869 | ''' | |
870 | def fget(self): | |
871 | ''' | |
872 | Returns the range | |
873 | ''' | |
874 | return self.__range | |
875 | ||
876 | def fset(self, x_range): | |
877 | ''' | |
878 | Controls the input of a valid type and generate the range | |
879 | ''' | |
880 | # if passed a simple number convert to tuple | |
881 | if type(x_range) in NUMTYPES: | |
882 | x_range = (x_range,) | |
883 | ||
884 | # A list, just convert to float | |
885 | if type(x_range) is list and len(x_range) > 0: | |
886 | # Convert all to float | |
887 | x_range = map(float, x_range) | |
888 | # Prevents repeated values and convert back to list | |
889 | self.__range = list(set(x_range[:])) | |
890 | # Sort the list to ascending order | |
891 | self.__range.sort() | |
892 | ||
893 | # A tuple, must check the lengths and generate the values | |
894 | elif type(x_range) is tuple and len(x_range) in (1,2,3): | |
895 | # Convert all to float | |
896 | x_range = map(float, x_range) | |
897 | ||
898 | # Inital values | |
899 | start = 0.0 | |
900 | step = 1.0 | |
901 | end = 0.0 | |
902 | ||
903 | # Only the end and it can't be less or iqual to 0 | |
904 | if len(x_range) is 1 and x_range > 0: | |
905 | end = x_range[0] | |
906 | ||
907 | # The start and the end but the start must be lesser then the end | |
908 | elif len(x_range) is 2 and x_range[0] < x_range[1]: | |
909 | start = x_range[0] | |
910 | end = x_range[1] | |
911 | ||
912 | # All 3, but the start must be lesser then the end | |
913 | elif x_range[0] < x_range[1]: | |
914 | start = x_range[0] | |
915 | end = x_range[1] | |
916 | step = x_range[2] | |
917 | ||
918 | # Starts the range | |
919 | self.__range = [] | |
920 | # Generate the range | |
921 | # Cnat use the range function becouse it don't suport float values | |
922 | while start <= end: | |
923 | self.__range.append(start) | |
924 | start += step | |
925 | ||
926 | # Incorrect type | |
927 | else: | |
928 | raise Exception, "x_range must be a list with one or more item or a tuple with 2 or 3 items" | |
929 | ||
930 | return property(**locals()) | |
931 | ||
932 | @apply | |
933 | def group_list(): | |
934 | doc = ''' | |
935 | The group_list is a read/write property used to pre-process the list | |
936 | of Groups. | |
937 | It can be: | |
938 | - a single number, point or lambda, will be converted to a single | |
939 | Group of one Data; | |
940 | - a list of numbers, will be converted to a group of numbers; | |
941 | - a list of tuples, will converted to a single Group of points; | |
942 | - a list of lists of numbers, each 'sublist' will be converted to | |
943 | a group of numbers; | |
944 | - a list of lists of tuples, each 'sublist' will be converted to a | |
945 | group of points; | |
946 | - a list of lists of lists, the content of the 'sublist' will be | |
947 | processed as coordinated lists and the result will be converted | |
948 | to a group of points; | |
949 | - a list of lambdas, each lambda represents a Group; | |
950 | - a Dictionary where each item can be the same of the list: number, | |
951 | point, list of numbers, list of points, list of lists | |
952 | (coordinated lists) or lambdas | |
953 | - an instance of Data; | |
954 | - an instance of group. | |
955 | ||
956 | Usage: | |
957 | >>> s = Series() | |
958 | >>> s.group_list = [1,2,3,4]; print s | |
959 | ["Group 1 ['1', '2', '3', '4']"] | |
960 | >>> s.group_list = [[1,2,3],[4,5,6]]; print s | |
961 | ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"] | |
962 | >>> s.group_list = (1,2); print s | |
963 | ["Group 1 ['(1, 2)']"] | |
964 | >>> s.group_list = [(1,2),(2,3)]; print s | |
965 | ["Group 1 ['(1, 2)', '(2, 3)']"] | |
966 | >>> s.group_list = [[(1,2),(2,3)],[(4,5),(5,6)]]; print s | |
967 | ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"] | |
968 | >>> s.group_list = [[[1,2,3],[1,2,3],[1,2,3]]]; print s | |
969 | ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"] | |
970 | >>> s.group_list = [(0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)]; print s | |
971 | ["Group 1 ['(0.5, 5.5)']", "Group 2 ['(0, 4)', '(6, 8)']", "Group 3 ['(5.5, 7)']", "Group 4 ['(7, 9)']"] | |
972 | >>> s.group_list = {'g1':[1,2,3], 'g2':[4,5,6]}; print s | |
973 | ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"] | |
974 | >>> s.group_list = {'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]}; print s | |
975 | ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"] | |
976 | >>> s.group_list = {'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]}; print s | |
977 | ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"] | |
978 | >>> s.range = 10 | |
979 | >>> s.group_list = lambda x:x*2 | |
980 | >>> s.group_list = [lambda x:x*2, lambda x:x**2, lambda x:x**3]; print s | |
981 | ["Group 1 ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "Group 2 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']", "Group 3 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']"] | |
982 | >>> s.group_list = {'linear':lambda x:x*2, 'square':lambda x:x**2, 'cubic':lambda x:x**3}; print s | |
983 | ["cubic ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']", "linear ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "square ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']"] | |
984 | >>> s.group_list = Data(1,'d1'); print s | |
985 | ["Group 1 ['d1: 1']"] | |
986 | >>> s.group_list = Group([(1,2),(2,3)],'g1'); print s | |
987 | ["g1 ['(1, 2)', '(2, 3)']"] | |
988 | ''' | |
989 | def fget(self): | |
990 | ''' | |
991 | Return the group list. | |
992 | ''' | |
993 | return self.__group_list | |
994 | ||
995 | def fset(self, series): | |
996 | ''' | |
997 | Controls the input of a valid group list. | |
998 | ''' | |
999 | #TODO: Add support to the following strem of data: [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)] | |
1000 | ||
1001 | # Type: None | |
1002 | if series is None: | |
1003 | self.__group_list = [] | |
1004 | ||
1005 | # List or Tuple | |
1006 | elif type(series) in LISTTYPES: | |
1007 | self.__group_list = [] | |
1008 | ||
1009 | is_function = lambda x: callable(x) | |
1010 | # Groups | |
1011 | if list in map(type, series) or max(map(is_function, series)): | |
1012 | for group in series: | |
1013 | self.add_group(group) | |
1014 | ||
1015 | # single group | |
1016 | else: | |
1017 | self.add_group(series) | |
1018 | ||
1019 | #old code | |
1020 | ## List of numbers | |
1021 | #if type(series[0]) in NUMTYPES or type(series[0]) is tuple: | |
1022 | # print series | |
1023 | # self.add_group(series) | |
1024 | # | |
1025 | ## List of anything else | |
1026 | #else: | |
1027 | # for group in series: | |
1028 | # self.add_group(group) | |
1029 | ||
1030 | # Dict representing series of groups | |
1031 | elif type(series) is dict: | |
1032 | self.__group_list = [] | |
1033 | names = series.keys() | |
1034 | names.sort() | |
1035 | for name in names: | |
1036 | self.add_group(Group(series[name],name,self)) | |
1037 | ||
1038 | # A single lambda | |
1039 | elif callable(series): | |
1040 | self.__group_list = [] | |
1041 | self.add_group(series) | |
1042 | ||
1043 | # Int/float, instance of Group or Data | |
1044 | elif type(series) in NUMTYPES or isinstance(series, Group) or isinstance(series, Data): | |
1045 | self.__group_list = [] | |
1046 | self.add_group(series) | |
1047 | ||
1048 | # Default | |
1049 | else: | |
1050 | raise TypeError, "Serie type not supported" | |
1051 | ||
1052 | return property(**locals()) | |
1053 | ||
1054 | def add_group(self, group, name=None): | |
1055 | ''' | |
1056 | Append a new group in group_list | |
1057 | ''' | |
1058 | if not isinstance(group, Group): | |
1059 | #Try to convert | |
1060 | group = Group(group, name, self) | |
1061 | ||
1062 | if len(group.data_list) is not 0: | |
1063 | # Auto naming groups | |
1064 | if group.name is None: | |
1065 | group.name = "Group "+str(len(self.__group_list)+1) | |
1066 | ||
1067 | self.__group_list.append(group) | |
1068 | self.__group_list[-1].parent = self | |
1069 | ||
1070 | def copy(self): | |
1071 | ''' | |
1072 | Returns a copy of the Series | |
1073 | ''' | |
1074 | new_series = Series() | |
1075 | new_series.__name = self.__name | |
1076 | if self.__range is not None: | |
1077 | new_series.__range = self.__range[:] | |
1078 | #Add color property in the copy method | |
1079 | #self.__colors = None | |
1080 | ||
1081 | for group in self: | |
1082 | new_series.add_group(group.copy()) | |
1083 | ||
1084 | return new_series | |
1085 | ||
1086 | def get_names(self): | |
1087 | ''' | |
1088 | Returns a list of the names of all groups in the Serie | |
1089 | ''' | |
1090 | names = [] | |
1091 | for group in self: | |
1092 | if group.name is None: | |
1093 | names.append('Group '+str(group.index()+1)) | |
1094 | else: | |
1095 | names.append(group.name) | |
1096 | ||
1097 | return names | |
1098 | ||
1099 | def to_list(self): | |
1100 | ''' | |
1101 | Returns a list with the content of all groups and data | |
1102 | ''' | |
1103 | big_list = [] | |
1104 | for group in self: | |
1105 | for data in group: | |
1106 | if type(data.content) in NUMTYPES: | |
1107 | big_list.append(data.content) | |
1108 | else: | |
1109 | big_list = big_list + list(data.content) | |
1110 | return big_list | |
1111 | ||
1112 | def __getitem__(self, key): | |
1113 | ''' | |
1114 | Makes the Series iterable, based in the group_list property | |
1115 | ''' | |
1116 | return self.__group_list[key] | |
1117 | ||
1118 | def __str__(self): | |
1119 | ''' | |
1120 | Returns a string that represents the Series | |
1121 | ''' | |
1122 | ret = "" | |
1123 | if self.name is not None: | |
1124 | ret += self.name + " " | |
1125 | if len(self) > 0: | |
1126 | list_str = [str(item) for item in self] | |
1127 | ret += str(list_str) | |
1128 | else: | |
1129 | ret += "[]" | |
1130 | return ret | |
1131 | ||
1132 | def __len__(self): | |
1133 | ''' | |
1134 | Returns the length of the Series, based in the group_lsit property | |
1135 | ''' | |
1136 | return len(self.group_list) | |
1137 | ||
1138 | ||
1139 | if __name__ == '__main__': | |
1140 | doctest.testmod() |