~alf-rodrigo/cairoplot/trunk

« back to all changes in this revision

Viewing changes to trunk/cairoplot.py

  • Committer: Rodrigo Moreira Araujo
  • Date: 2009-03-08 20:28:42 UTC
  • Revision ID: rodrigo@scrooge-20090308202842-b2496nkb4ts4ru5m
CairoPlot.py: 
 - Code revision and refactoring;
 - The BarPlot class is now a base class on top of which the classes HorizontalBarPlot and VerticalBarPlot are built
 - Color themes are now available;
 - PiePlot and DonutPlot data is now ordered;
 - Horizontal label collision problem was solved for BarPlots;
 - Axis titles are now allowed for ScatterPlot, DotLinePlot and FunctionPlot classes.

tests.py:
 - Theme tests were added;
 - BarPlot tests were changed into HorizontalBarPlot and VerticalBarPlot tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
#Contributor: João S. O. Bueno
26
26
 
27
27
#TODO: review the whole code
 
28
#stopped at render method on Scatter Plot
28
29
 
29
30
__version__ = 1.1
30
31
 
41
42
          "yellow" : (1.0,1.0,0.0,1.0), "magenta" : (1.0,0.0,1.0,1.0), "cyan"   : (0.0,1.0,1.0,1.0),
42
43
          "orange" : (1.0,0.5,0.0,1.0), "white"   : (1.0,1.0,1.0,1.0), "black"  : (0.0,0.0,0.0,1.0)}
43
44
 
44
 
THEMES = {"black_red"         : [ (0.0,0.0,0.0,1.0), (1.0,0.0,0.0,1.0) ],
45
 
          "red_green_blue"    : [ (1.0,0.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0) ],
46
 
          "red_orange_yellow" : [ (1.0,0.2,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,1.0,0.0,1.0) ],
47
 
          "yellow_orange_red" : [ (1.0,1.0,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,0.2,0.0,1.0) ],
48
 
          "rainbow"           : [ (1.0,0.0,0.0,1.0), (1.0,0.5,0.0,1.0), (1.0,1.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0), (0.3, 0.0, 0.5,1.0), (0.5, 0.0, 1.0, 1.0) ] }
 
45
THEMES = {"black_red"         : [(0.0,0.0,0.0,1.0), (1.0,0.0,0.0,1.0)],
 
46
          "red_green_blue"    : [(1.0,0.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0)],
 
47
          "red_orange_yellow" : [(1.0,0.2,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,1.0,0.0,1.0)],
 
48
          "yellow_orange_red" : [(1.0,1.0,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,0.2,0.0,1.0)],
 
49
          "rainbow"           : [(1.0,0.0,0.0,1.0), (1.0,0.5,0.0,1.0), (1.0,1.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0), (0.3, 0.0, 0.5,1.0), (0.5, 0.0, 1.0, 1.0)]}
49
50
 
50
51
def colors_from_theme( theme, series_length ):
51
52
    colors = []
52
53
    if theme not in THEMES.keys() :
53
54
        raise Exception, "Theme not defined" 
54
 
    color_steps = THEMES[ theme ]
55
 
    steps_length = len(color_steps)
56
 
    if series_length <= steps_length:
57
 
        colors = [color for color in color_steps[0:steps_length] ]
 
55
    color_steps = THEMES[theme]
 
56
    n_colors = len(color_steps)
 
57
    if series_length <= n_colors:
 
58
        colors = [color for color in color_steps[0:n_colors]]
58
59
    else:
59
 
        iterations      = [ (series_length - steps_length)/(steps_length - 1) for i in color_steps[:-1] ]
60
 
        over_iterations = (series_length - steps_length) % (steps_length - 1)
61
 
        for i in range( steps_length - 1 ):
 
60
        iterations = [(series_length - n_colors)/(n_colors - 1) for i in color_steps[:-1]]
 
61
        over_iterations = (series_length - n_colors) % (n_colors - 1)
 
62
        for i in range(n_colors - 1):
62
63
            if over_iterations <= 0:
63
64
                break
64
65
            iterations[i] += 1
65
66
            over_iterations -= 1
66
 
        for index,color in enumerate( color_steps[:-1] ):
 
67
        for index,color in enumerate(color_steps[:-1]):
67
68
            colors.append(color)
68
69
            if iterations[index] == 0:
69
70
                continue
70
71
            next_color = color_steps[index+1]
71
 
            color_step = ( (next_color[0] - color[0])/(iterations[index] + 1),
72
 
                           (next_color[1] - color[1])/(iterations[index] + 1),
73
 
                           (next_color[2] - color[2])/(iterations[index] + 1),
74
 
                           (next_color[3] - color[3])/(iterations[index] + 1) )
 
72
            color_step = ((next_color[0] - color[0])/(iterations[index] + 1),
 
73
                          (next_color[1] - color[1])/(iterations[index] + 1),
 
74
                          (next_color[2] - color[2])/(iterations[index] + 1),
 
75
                          (next_color[3] - color[3])/(iterations[index] + 1))
75
76
            for i in range( iterations[index] ):
76
 
                colors.append( (color[0] + color_step[0]*(i+1), 
77
 
                                color[1] + color_step[1]*(i+1), 
78
 
                                color[2] + color_step[2]*(i+1),
79
 
                                color[3] + color_step[3]*(i+1)) )
 
77
                colors.append((color[0] + color_step[0]*(i+1), 
 
78
                               color[1] + color_step[1]*(i+1), 
 
79
                               color[2] + color_step[2]*(i+1),
 
80
                               color[3] + color_step[3]*(i+1)))
80
81
        colors.append(color_steps[-1])
81
 
        
82
82
    return colors
83
83
        
84
84
 
90
90
        return HORZ
91
91
 
92
92
#Class definition
 
93
 
93
94
class Plot(object):
94
95
    def __init__(self, 
95
96
                 surface=None,
101
102
                 x_labels = None,
102
103
                 y_labels = None,
103
104
                 series_colors = None):
104
 
        
105
105
        random.seed(2)
106
 
    
107
106
        self.create_surface(surface, width, height)
108
107
        self.width = width
109
108
        self.height = height
110
109
        self.context = cairo.Context(self.surface)
111
 
        
112
110
        self.labels={}
113
111
        self.labels[HORZ] = x_labels
114
112
        self.labels[VERT] = y_labels
115
 
        
116
113
        self.load_series(data, x_labels, y_labels, series_colors)
117
 
 
118
 
 
119
114
        self.font_size = 10
120
 
        
121
115
        self.set_background (background)
122
116
        self.border = border
123
117
        self.borders = {}
124
 
        
125
118
        self.line_color = (0.5, 0.5, 0.5)
126
119
        self.line_width = 0.5
127
120
        self.label_color = (0.0, 0.0, 0.0)
128
121
        self.grid_color = (0.8, 0.8, 0.8)
129
 
        
130
122
    
131
123
    def create_surface(self, surface, width=None, height=None):
132
124
        self.filename = None
147
139
            if sufix != "svg":
148
140
                self.filename += ".svg"
149
141
            self.surface = cairo.SVGSurface(self.filename, width, height)
150
 
    
151
 
    #def __del__(self):
152
 
    #    self.commit()
153
142
 
154
143
    def commit(self):
155
144
        try:
167
156
        
168
157
        #data can be a list, a list of lists or a dictionary with 
169
158
        #each item as a labeled data series.
170
 
        #we should (for teh time being) create a list of lists
 
159
        #we should (for the time being) create a list of lists
171
160
        #and set labels for teh series rom  teh values provided.
172
161
        
173
162
        self.series_labels = []
174
163
        self.data = []
175
 
        #if we have labeled series:
 
164
        #dictionary
176
165
        if hasattr(data, "keys"):
177
 
            #dictionary:
178
166
            self.series_labels = data.keys()
179
167
            for key in self.series_labels:
180
168
                self.data.append(data[key])
181
 
        #if we have a series of series:
182
 
        #changed the following line to adapt the Plot class to work
183
 
        #with GanttChart class
184
 
        #elif hasattr(data[0], "__getitem__"):
 
169
        #lists of lists:
185
170
        elif max([hasattr(item,'__delitem__') for item in data]) :
186
171
            self.data = data
187
172
            self.series_labels = range(len(data))
 
173
        #list
188
174
        else:
189
175
            self.data = [data]
190
176
            self.series_labels = None
191
 
 
 
177
        #TODO: allow user passed series_widths
192
178
        self.series_widths = [1.0 for series in self.data]
193
179
        self.process_colors( series_colors )
194
180
        
196
182
        #series_colors might be None, a theme, a string of colors names or a string of color tuples
197
183
        if length is None :
198
184
            length = len( self.data )
199
 
        #Randomize colors
 
185
        #no colors passed
200
186
        if not series_colors:
 
187
            #Randomize colors
201
188
            self.series_colors = [ [random.random() for i in range(3)] + [1.0]  for series in range( length ) ]
202
189
        else:
203
190
            #Theme pattern
217
204
 
218
205
    def get_width(self):
219
206
        return self.surface.get_width()
 
207
    
220
208
    def get_height(self):
221
209
        return self.surface.get_height()
222
210
 
232
220
                raise TypeError ("Background should be either cairo.LinearGradient or a 3-tuple, not %s" % type(background))
233
221
        
234
222
    def render_background(self):
235
 
        if isinstance (self.background, cairo.LinearGradient):
 
223
        if isinstance(self.background, cairo.LinearGradient):
236
224
            self.context.set_source(self.background)
237
225
        else:
238
226
            self.context.set_source_rgba(*self.background)
245
233
        self.context.rectangle(self.border, self.border,
246
234
                               self.width - 2 * self.border,
247
235
                               self.height - 2 * self.border)
248
 
        #CORRECTION: Added the next line so it will draw the outline of the bounding box
249
236
        self.context.stroke()
250
237
 
251
238
    def render(self):
281
268
        self.bounds[HORZ] = x_bounds
282
269
        self.bounds[VERT] = y_bounds
283
270
        self.bounds[NORM] = z_bounds
284
 
        
285
271
        self.titles = {}
286
272
        self.titles[HORZ] = x_title
287
273
        self.titles[VERT] = y_title
288
 
        
289
274
        self.max_value = {}
290
 
        
291
275
        self.axis = axis
292
276
        self.discrete = discrete
293
277
        self.dots = dots
294
278
        self.grid = grid
295
279
        self.series_legend = series_legend
296
280
        self.variable_radius = False
297
 
        
298
 
        Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors)
299
 
        
300
281
        self.x_label_angle = math.pi / 2.5
301
282
        self.circle_colors = circle_colors
302
283
        
 
284
        Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors)
 
285
        
303
286
        self.dash = None
304
287
        if dash:
305
288
            if hasattr(dash, "keys"):
312
295
        self.load_errors(errorx, errory)
313
296
    
314
297
    def convert_list_to_tuple(self, data):
315
 
        out_data = []
316
 
        for index, item in enumerate(data[0]):
317
 
            if len(data) == 3:
318
 
                self.variable_radius = False
319
 
                tuple = (item, data[1][index], data[2][index])
320
 
            else:
321
 
                tuple = (item, data[1][index])
322
 
            out_data.append( tuple )
 
298
        #Data must be converted from lists of coordinates to a single
 
299
        # list of tuples
 
300
        out_data = zip(*data)
 
301
        if len(data) == 3:
 
302
            self.variable_radius = True
323
303
        return out_data
324
304
    
325
305
    def load_series(self, data, x_labels = None, y_labels = None, series_colors=None):
328
308
            if hasattr( data.values()[0][0], "__delitem__" ) :
329
309
                for key in data.keys() :
330
310
                    data[key] = self.convert_list_to_tuple(data[key])
331
 
            else:
332
 
                if len(data.values()[0][0]) == 3:
 
311
            elif len(data.values()[0][0]) == 3:
333
312
                    self.variable_radius = True
334
 
        
 
313
        #List
335
314
        elif hasattr(data[0], "__delitem__") :
336
 
            #List of lists
 
315
            #List of lists 
337
316
            if hasattr(data[0][0], "__delitem__") :
338
317
                for index,value in enumerate(data) :
339
318
                    data[index] = self.convert_list_to_tuple(value)
340
319
            #List
341
320
            elif type(data[0][0]) != type((0,0)):
342
 
                data = self.convert_list_to_tuple(data)            
 
321
                data = self.convert_list_to_tuple(data)
 
322
            #Three dimensional data
343
323
            elif len(data[0][0]) == 3:
344
324
                self.variable_radius = True
345
 
                
 
325
        #List with three dimensional tuples
346
326
        elif len(data[0]) == 3:
347
327
            self.variable_radius = True
348
 
    
349
328
        Plot.load_series(self, data, x_labels, y_labels, series_colors)
350
329
        self.calc_boundaries()
351
330
        self.calc_labels()
354
333
        self.errors = None
355
334
        if errorx == None and errory == None:
356
335
            return
357
 
        
358
336
        self.errors = {}
359
337
        self.errors[HORZ] = None
360
338
        self.errors[VERT] = None
 
339
        #asimetric errors
361
340
        if errorx and hasattr(errorx[0], "__delitem__"):
362
341
            self.errors[HORZ] = errorx
 
342
        #simetric errors
363
343
        elif errorx:
364
344
            self.errors[HORZ] = [errorx]
 
345
        #asimetric errors
365
346
        if errory and hasattr(errory[0], "__delitem__"):
366
347
            self.errors[VERT] = errory
 
348
        #simetric errors
367
349
        elif errory:
368
350
            self.errors[VERT] = [errory]
369
351
    
370
352
    def calc_labels(self):
371
353
        if not self.labels[HORZ]:
372
354
            amplitude = self.bounds[HORZ][1] - self.bounds[HORZ][0]
373
 
            if amplitude % 10: #if vertical labels need floating points
 
355
            if amplitude % 10: #if horizontal labels need floating points
374
356
                self.labels[HORZ] = ["%.2lf" % (float(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ]
375
357
            else:
376
 
                self.labels[HORZ] = [str(int(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ]
 
358
                self.labels[HORZ] = ["%d" % (int(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ]
377
359
        if not self.labels[VERT]:
378
360
            amplitude = self.bounds[VERT][1] - self.bounds[VERT][0]
379
361
            if amplitude % 10: #if vertical labels need floating points
380
362
                self.labels[VERT] = ["%.2lf" % (float(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ]
381
363
            else:
382
 
                self.labels[VERT] = [str(int(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ]
 
364
                self.labels[VERT] = ["%d" % (int(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ]
383
365
 
384
366
    def calc_extents(self, direction):
385
367
        self.context.set_font_size(self.font_size * 0.8)
387
369
        self.borders[other_direction(direction)] = self.max_value[direction] + self.border + 20
388
370
 
389
371
    def calc_boundaries(self):
 
372
        #HORZ = 0, VERT = 1, NORM = 2
390
373
        min_data_value = [0,0,0]
391
374
        max_data_value = [0,0,0]
392
375
        for serie in self.data :
393
376
            for tuple in serie :
394
 
                if tuple[0] > max_data_value[HORZ]: #horizontal
395
 
                    max_data_value[HORZ] = tuple[0]
396
 
                if tuple[0] < min_data_value[HORZ]:
397
 
                    min_data_value[HORZ] = tuple[0]
398
 
                if tuple[1] > max_data_value[VERT]: #vertical
399
 
                    max_data_value[VERT] = tuple[1]
400
 
                if tuple[1] < min_data_value[VERT]:
401
 
                    min_data_value[VERT] = tuple[1]
402
 
                if self.variable_radius and tuple[2] > max_data_value[NORM]: #normal
403
 
                    max_data_value[NORM] = tuple[2]
404
 
                if self.variable_radius and tuple[2] < min_data_value[NORM]:
405
 
                    min_data_value[NORM] = tuple[2]
 
377
                for index, item in enumerate(tuple) :
 
378
                    if item > max_data_value[index]:
 
379
                        max_data_value[index] = item
 
380
                    elif item < min_data_value[index]:
 
381
                        min_data_value[index] = item
406
382
                
407
383
        if not self.bounds[HORZ]:
408
384
            self.bounds[HORZ] = (min_data_value[HORZ], max_data_value[HORZ])
419
395
        self.plot_width = self.width - 2* self.borders[HORZ]
420
396
        
421
397
        self.plot_top = self.height - self.borders[VERT]
422
 
        
423
 
        series_amplitude = [0,0,0]
424
 
        series_amplitude[HORZ] = self.bounds[HORZ][1] - self.bounds[HORZ][0]
425
 
        series_amplitude[VERT] = self.bounds[VERT][1] - self.bounds[VERT][0]
426
 
        if self.variable_radius :
427
 
            series_amplitude[NORM] = self.bounds[NORM][1] - self.bounds[NORM][0]
428
 
        
 
398
                
 
399
    def calc_steps(self):
 
400
        #Calculates all the x, y, z and color steps
 
401
        series_amplitude = [self.bounds[index][1] - self.bounds[index][0] for index in range(3)]
 
402
 
429
403
        if series_amplitude[HORZ]:
430
404
            self.horizontal_step = float (self.plot_width) / series_amplitude[HORZ]
431
405
        else:
440
414
            if self.variable_radius:
441
415
                self.z_step = float (self.bounds[NORM][1]) / series_amplitude[NORM]
442
416
            if self.circle_colors:
443
 
                r = float( self.circle_colors[1][0] - self.circle_colors[0][0] ) / series_amplitude[NORM]
444
 
                g = float( self.circle_colors[1][1] - self.circle_colors[0][1] ) / series_amplitude[NORM]
445
 
                b = float( self.circle_colors[1][2] - self.circle_colors[0][2] ) / series_amplitude[NORM]
446
 
                a = float( self.circle_colors[1][3] - self.circle_colors[0][3] ) / series_amplitude[NORM]
447
 
                self.circle_color_step = ( r, g, b, a )
 
417
                self.circle_color_step = tuple([float(self.circle_colors[1][i]-self.circle_colors[0][i])/series_amplitude[NORM] for i in range(4)])
448
418
        else:
449
419
            self.z_step = 0.00
450
420
            self.circle_color_step = ( 0.0, 0.0, 0.0, 0.0 )
451
421
    
452
422
    def get_circle_color(self, value):
453
 
        r = self.circle_colors[0][0] + value*self.circle_color_step[0]
454
 
        g = self.circle_colors[0][1] + value*self.circle_color_step[1]
455
 
        b = self.circle_colors[0][2] + value*self.circle_color_step[2]
456
 
        a = self.circle_colors[0][3] + value*self.circle_color_step[3]
457
 
        return (r,g,b,a)
 
423
        return tuple( [self.circle_colors[0][i] + value*self.circle_color_step[i] for i in range(4)] )
458
424
    
459
425
    def render(self):
460
426
        self.calc_all_extents()
 
427
        self.calc_steps()
461
428
        self.render_background()
462
429
        self.render_bounding_box()
463
430
        if self.axis:
464
431
            self.render_axis()
465
432
        if self.grid:
466
433
            self.render_grid()
467
 
        self.render_labels()        
 
434
        self.render_labels()
468
435
        self.render_plot()
469
436
        if self.errors:
470
437
            self.render_errors()
472
439
            self.render_legend()
473
440
            
474
441
    def render_axis(self):
 
442
        #Draws both the axis lines and their titles
475
443
        cr = self.context
476
444
        cr.set_source_rgba(*self.line_color)
477
445
        cr.move_to(self.borders[HORZ], self.height - self.borders[VERT])
549
517
    def render_legend(self):
550
518
        cr = self.context
551
519
        cr.set_font_size(self.font_size)
552
 
        cr.set_line_width(self.line_width*1)
 
520
        cr.set_line_width(self.line_width)
553
521
 
554
522
        widest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[2])
 
523
        tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3])
555
524
        max_width = self.context.text_extents(widest_word)[2]
556
 
        tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3])
557
525
        max_height = self.context.text_extents(tallest_word)[3] * 1.1
558
526
        
559
527
        color_box_height = max_height / 2
560
528
        color_box_width = color_box_height * 2
561
529
        
562
 
        #Add a bounding box
 
530
        #Draw a bounding box
563
531
        bounding_box_width = max_width + color_box_width + 15
564
532
        bounding_box_height = (len(self.series_labels)+0.5) * max_height
565
533
        cr.set_source_rgba(1,1,1)
573
541
                            bounding_box_width, bounding_box_height)
574
542
        cr.stroke()
575
543
 
576
 
        i = 0
577
 
        for key in self.series_labels:
578
 
            #Create color box
579
 
            cr.set_source_rgba(*self.series_colors[i])
 
544
        for idx,key in enumerate(self.series_labels):
 
545
            #Draw color box
 
546
            cr.set_source_rgba(*self.series_colors[idx])
580
547
            cr.rectangle(self.width - self.borders[HORZ] - max_width - color_box_width - 10, 
581
 
                                self.borders[VERT] + color_box_height + (i*max_height) ,
 
548
                                self.borders[VERT] + color_box_height + (idx*max_height) ,
582
549
                                color_box_width, color_box_height)
583
550
            cr.fill()
584
551
            
585
552
            cr.set_source_rgba(0, 0, 0)
586
553
            cr.rectangle(self.width - self.borders[HORZ] - max_width - color_box_width - 10, 
587
 
                                self.borders[VERT] + color_box_height + (i*max_height),
 
554
                                self.borders[VERT] + color_box_height + (idx*max_height),
588
555
                                color_box_width, color_box_height)
589
556
            cr.stroke()
590
557
            
591
 
            # Create labels
 
558
            #Draw series labels
592
559
            cr.set_source_rgba(0, 0, 0)
593
 
            cr.move_to(self.width - self.borders[HORZ] - max_width - 5, self.borders[VERT] + ((i+1)*max_height))
 
560
            cr.move_to(self.width - self.borders[HORZ] - max_width - 5, self.borders[VERT] + ((idx+1)*max_height))
594
561
            cr.show_text(key)
595
 
            i += 1
596
562
 
597
563
    def render_errors(self):
598
564
        cr = self.context
688
654
                        cr.set_dash([])
689
655
                    last_tuple = tuple
690
656
 
691
 
 
692
657
class DotLinePlot(ScatterPlot):
693
658
    def __init__(self, 
694
659
                 surface=None,
770
735
        
771
736
        #This function converts a function, a list of functions or a dictionary
772
737
        #of functions into its corresponding array of data
773
 
        
774
738
        data = None
775
 
        
776
739
        #if no bounds are provided
777
740
        if x_bounds == None:
778
741
            x_bounds = (0,10)
779
 
        
780
 
        #dictionary:
781
 
        if hasattr(function, "keys"):
 
742
 
 
743
        if hasattr(function, "keys"): #dictionary:
782
744
            data = {}
783
745
            for key in function.keys():
784
746
                data[ key ] = []
786
748
                while i <= x_bounds[1] :
787
749
                    data[ key ].append( function[ key ](i) )
788
750
                    i += self.step
789
 
        
790
 
        #list of functions
791
 
        elif hasattr( function,'__delitem__' ) :
 
751
        elif hasattr(function, "__delitem__"): #list of functions
792
752
            data = []
793
753
            for index,f in enumerate( function ) : 
794
754
                data.append( [] )
796
756
                while i <= x_bounds[1] :
797
757
                    data[ index ].append( f(i) )
798
758
                    i += self.step
799
 
        
800
 
        #function
801
 
        else:
 
759
        else: #function
802
760
            data = []
803
761
            i = x_bounds[0]
804
762
            while i <= x_bounds[1] :
859
817
        self.bounds = {}
860
818
        self.bounds[HORZ] = x_bounds
861
819
        self.bounds[VERT] = y_bounds
862
 
 
863
 
        Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors)
864
820
        self.grid = grid
865
821
        self.rounded_corners = rounded_corners
866
822
        self.three_dimension = three_dimension
867
 
 
 
823
        self.x_label_angle = math.pi / 2.5
868
824
        self.max_value = {}
869
825
 
 
826
        Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors)
 
827
 
870
828
    def load_series(self, data, x_labels = None, y_labels = None, series_colors = None):
871
829
        Plot.load_series(self, data, x_labels, y_labels, series_colors)
872
830
        self.calc_boundaries()
873
831
        
874
832
    def process_colors(self, series_colors):
875
833
        #Data for a BarPlot might be a List or a List of Lists.
876
 
        #On the first case, colors must generated for all bars,
 
834
        #On the first case, colors must be generated for all bars,
877
835
        #On the second, colors must be generated for each of the inner lists.
878
836
        if hasattr(self.data[0], '__getitem__'):
879
837
            length = max(len(series) for series in self.data)
882
840
            
883
841
        Plot.process_colors( self, series_colors, length )
884
842
        
885
 
    def calc_boundaries(self):
886
 
        
887
 
        if not self.bounds[HORZ]:
888
 
            self.bounds[HORZ] = (0, len(self.data))
889
 
 
890
 
        if not self.bounds[VERT]:
891
 
            max_data_value = min_data_value = 0
892
 
            for series in self.data:
893
 
                if max(series) > max_data_value:
894
 
                    max_data_value = max(series)
895
 
                if min(series) < min_data_value:
896
 
                    min_data_value = min(series)
897
 
            self.bounds[VERT] = (min_data_value, max_data_value)
898
 
 
899
843
    def calc_extents(self, direction):
900
844
        self.max_value[direction] = 0
901
845
        if self.labels[direction]:
905
849
        else:
906
850
            self.borders[other_direction(direction)] = self.border
907
851
 
908
 
    def calc_horz_extents(self):
909
 
        self.calc_extents(HORZ)
910
 
 
911
 
    def calc_vert_extents(self):
912
 
        self.calc_extents(VERT)
913
 
        if self.labels[VERT] and not self.labels[HORZ]:
914
 
            self.borders[VERT] += 10
915
 
 
916
852
    def render(self):
917
853
        self.calc_horz_extents()
918
854
        self.calc_vert_extents()
948
884
        self.context.line_to(x0-shift, y0+shift)
949
885
        self.context.close_path()
950
886
 
 
887
    def render_ground(self):
 
888
        self.draw_3d_rectangle_front(self.borders[HORZ], self.height - self.borders[VERT], 
 
889
                                     self.width - self.borders[HORZ], self.height - self.borders[VERT] + 5, 10)
 
890
        self.context.fill()
 
891
 
 
892
        self.draw_3d_rectangle_side (self.borders[HORZ], self.height - self.borders[VERT], 
 
893
                                     self.width - self.borders[HORZ], self.height - self.borders[VERT] + 5, 10)
 
894
        self.context.fill()
 
895
 
 
896
        self.draw_3d_rectangle_top  (self.borders[HORZ], self.height - self.borders[VERT], 
 
897
                                     self.width - self.borders[HORZ], self.height - self.borders[VERT] + 5, 10)
 
898
        self.context.fill()
 
899
 
 
900
    def render_labels(self):
 
901
        self.context.set_font_size(self.font_size * 0.8)
 
902
 
 
903
        if self.labels[HORZ]:
 
904
            self.render_horz_labels()
 
905
        if self.labels[VERT]:
 
906
            self.render_vert_labels()
 
907
        
 
908
    def draw_rectangle(self, x0, y0, x1, y1):
 
909
        self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2)
 
910
        self.context.line_to(x1-5, y0)
 
911
        self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0)
 
912
        self.context.line_to(x1, y1-5)
 
913
        self.context.arc(x1-5, y1-5, 5, 0, math.pi/2)
 
914
        self.context.line_to(x0+5, y1)
 
915
        self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi)
 
916
        self.context.line_to(x0, y0+5)
 
917
        self.context.close_path()
 
918
 
 
919
class HorizontalBarPlot(BarPlot):
 
920
    def __init__(self, 
 
921
                 surface = None,
 
922
                 data = None,
 
923
                 width = 640,
 
924
                 height = 480,
 
925
                 background = None,
 
926
                 border = 0,
 
927
                 grid = False,
 
928
                 rounded_corners = False,
 
929
                 three_dimension = False,
 
930
                 x_labels = None,
 
931
                 y_labels = None,
 
932
                 x_bounds = None,
 
933
                 y_bounds = None,
 
934
                 series_colors = None):
 
935
 
 
936
        self.bounds = {}
 
937
        self.bounds[HORZ] = x_bounds
 
938
        self.bounds[VERT] = y_bounds
 
939
        self.grid = grid
 
940
        self.rounded_corners = rounded_corners
 
941
        self.three_dimension = three_dimension
 
942
        self.x_label_angle = math.pi / 2.5
 
943
        self.max_value = {}
 
944
 
 
945
        Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors)
 
946
 
 
947
    def calc_boundaries(self):
 
948
        if not self.bounds[HORZ]:
 
949
            max_data_value = max(max(serie) for serie in self.data)
 
950
            self.bounds[HORZ] = (0, max_data_value)
 
951
        if not self.bounds[VERT]:
 
952
            self.bounds[VERT] = (0, len(self.data))
 
953
 
 
954
    def calc_horz_extents(self):
 
955
        self.calc_extents(HORZ)
 
956
 
 
957
    def calc_vert_extents(self):
 
958
        self.calc_extents(VERT)
 
959
        if self.labels[HORZ] and not self.labels[VERT]:
 
960
            self.borders[HORZ] += 10
 
961
 
 
962
    def render_grid(self):
 
963
        self.context.set_source_rgba(0.8, 0.8, 0.8)
 
964
        if self.labels[HORZ]:
 
965
            self.context.set_font_size(self.font_size * 0.8)
 
966
            step = (self.width - 2*self.borders[HORZ])/(len(self.labels[HORZ])-1)
 
967
            x = self.borders[HORZ]
 
968
            next_x = 0
 
969
            for item in self.labels[HORZ]:
 
970
                width = self.context.text_extents(item)[2]
 
971
                if x - width/2 > next_x and x - width/2 > self.border:
 
972
                    self.context.move_to(x, self.border)
 
973
                    self.context.line_to(x, self.height - self.borders[VERT])
 
974
                    self.context.stroke()
 
975
                    next_x = x + width/2
 
976
                x += step
 
977
        else:
 
978
            lines = 10
 
979
            horizontal_step = float(self.width - 2*self.borders[HORZ])/(lines-1)
 
980
            x = self.borders[HORZ]
 
981
            for y in xrange(0, lines):
 
982
                self.context.move_to(x, self.border)
 
983
                self.context.line_to(x, self.height - self.borders[VERT])
 
984
                self.context.stroke()
 
985
                x += horizontal_step
 
986
 
 
987
    def render_horz_labels(self):
 
988
        step = (self.width - 2*self.borders[HORZ])/(len(self.labels[HORZ])-1)
 
989
        x = self.borders[HORZ]
 
990
        next_x = 0
 
991
 
 
992
        for item in self.labels[HORZ]:
 
993
            self.context.set_source_rgba(*self.label_color)
 
994
            width = self.context.text_extents(item)[2]
 
995
            if x - width/2 > next_x and x - width/2 > self.border:
 
996
                self.context.move_to(x - width/2, self.height - self.borders[VERT] + self.max_value[HORZ] + 3)
 
997
                self.context.show_text(item)
 
998
                next_x = x + width/2
 
999
            x += step
 
1000
            
 
1001
    def render_vert_labels(self):
 
1002
        step = (self.height - self.borders[VERT] - self.border)/(len(self.labels[VERT]))
 
1003
        y = self.border + step/2
 
1004
 
 
1005
        for item in self.labels[VERT]:
 
1006
            self.context.set_source_rgba(*self.label_color)
 
1007
            width, height = self.context.text_extents(item)[2:4]
 
1008
            self.context.move_to(self.borders[HORZ] - width - 5, y + height/2)
 
1009
            self.context.show_text(item)
 
1010
            y += step
 
1011
        self.labels[VERT].reverse()
 
1012
        
 
1013
    def draw_rectangle(self, x0, y0, x1, y1):
 
1014
        self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2)
 
1015
        self.context.line_to(x1-5, y0)
 
1016
        self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0)
 
1017
        self.context.line_to(x1, y1-5)
 
1018
        self.context.arc(x1-5, y1-5, 5, 0, math.pi/2)
 
1019
        self.context.line_to(x0+5, y1)
 
1020
        self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi)
 
1021
        self.context.line_to(x0, y0+5)
 
1022
        self.context.close_path()
 
1023
 
 
1024
    def render_plot(self):
 
1025
        plot_width = self.width - 2*self.borders[HORZ]
 
1026
        plot_height = self.height - self.borders[VERT] - self.border
 
1027
        plot_top = self.height - self.borders[VERT]
 
1028
 
 
1029
        series_amplitude = self.bounds[HORZ][1] - self.bounds[HORZ][0]
 
1030
 
 
1031
        x0 = self.borders[HORZ]
 
1032
        
 
1033
        vertical_step = float(plot_height)/len(self.data)
 
1034
        if series_amplitude:
 
1035
            horizontal_step = float(plot_width)/series_amplitude
 
1036
        else:
 
1037
            horizontal_step = 0.00
 
1038
 
 
1039
        for i,series in enumerate(self.data):
 
1040
            inner_step = vertical_step/(len(series) + 0.4)
 
1041
            y0 = self.border + i*vertical_step + 0.2*inner_step
 
1042
            for number,key in enumerate(series):
 
1043
                linear = cairo.LinearGradient( key*horizontal_step/2, y0, key*horizontal_step/2, y0 + inner_step,  )
 
1044
                #FIXME: test if set_source_rgba accepts 3 parameters
 
1045
                color = self.series_colors[number]
 
1046
                linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0)
 
1047
                linear.add_color_stop_rgba(1.0, *color)
 
1048
                self.context.set_source(linear)
 
1049
                
 
1050
                if self.rounded_corners and key != 0:
 
1051
                    self.draw_rectangle(x0, y0, x0 + key*horizontal_step, y0 + inner_step)
 
1052
                    self.context.fill()
 
1053
                else:
 
1054
                    self.context.rectangle(x0, y0, key*horizontal_step, inner_step)
 
1055
                    self.context.fill()
 
1056
                
 
1057
                y0 += inner_step
 
1058
    
 
1059
class VerticalBarPlot(BarPlot):
 
1060
    def __init__(self, 
 
1061
                 surface = None,
 
1062
                 data = None,
 
1063
                 width = 640,
 
1064
                 height = 480,
 
1065
                 background = None,
 
1066
                 border = 0,
 
1067
                 grid = False,
 
1068
                 rounded_corners = False,
 
1069
                 three_dimension = False,
 
1070
                 x_labels = None,
 
1071
                 y_labels = None,
 
1072
                 x_bounds = None,
 
1073
                 y_bounds = None,
 
1074
                 series_colors = None):
 
1075
 
 
1076
        BarPlot.__init__(self, surface, data, width, height, background, border, grid, rounded_corners, three_dimension,
 
1077
                         x_labels, y_labels, x_bounds, y_bounds, series_colors)
 
1078
 
 
1079
    def calc_boundaries(self):
 
1080
        if not self.bounds[HORZ]:
 
1081
            self.bounds[HORZ] = (0, len(self.data))
 
1082
        if not self.bounds[VERT]:
 
1083
            max_data_value = max(max(serie) for serie in self.data)
 
1084
            self.bounds[VERT] = (0, max_data_value)
 
1085
 
 
1086
    def calc_horz_extents(self):
 
1087
        self.calc_extents(HORZ)
 
1088
 
 
1089
    def calc_vert_extents(self):
 
1090
        self.calc_extents(VERT)
 
1091
        if self.labels[VERT] and not self.labels[HORZ]:
 
1092
            self.borders[VERT] += 10
 
1093
 
951
1094
    def render_grid(self):
952
1095
        self.context.set_source_rgba(0.8, 0.8, 0.8)
953
1096
        if self.labels[VERT]:
961
1104
            self.context.line_to(self.width - self.border, y)
962
1105
            self.context.stroke()
963
1106
            y += vertical_step
964
 
 
 
1107
            
965
1108
    def render_ground(self):
966
1109
        self.draw_3d_rectangle_front(self.borders[HORZ], self.height - self.borders[VERT], 
967
1110
                                     self.width - self.borders[HORZ], self.height - self.borders[VERT] + 5, 10)
975
1118
                                     self.width - self.borders[HORZ], self.height - self.borders[VERT] + 5, 10)
976
1119
        self.context.fill()
977
1120
 
978
 
    def render_labels(self):
979
 
        self.context.set_font_size(self.font_size * 0.8)
980
 
 
981
 
        if self.labels[HORZ]:
982
 
            self.render_horz_labels()
983
 
        if self.labels[VERT]:
984
 
            self.render_vert_labels()
985
 
 
986
 
    def render_labels(self):
987
 
        self.context.set_font_size(self.font_size * 0.8)
988
 
 
989
 
        if self.labels[HORZ]:
990
 
            self.render_horz_labels()
991
 
        if self.labels[VERT]:
992
 
            self.render_vert_labels()
993
 
 
994
1121
    def render_horz_labels(self):
995
1122
        step = (self.width - self.borders[HORZ] - self.border)/len(self.labels[HORZ])
996
1123
        x = self.borders[HORZ] + step/2
 
1124
        next_x = 0
997
1125
 
998
1126
        for item in self.labels[HORZ]:
999
1127
            self.context.set_source_rgba(*self.label_color)
1000
1128
            width = self.context.text_extents(item)[2]
1001
 
            self.context.move_to(x - width/2, self.height - self.borders[VERT] + self.max_value[HORZ] + 3)
1002
 
            self.context.show_text(item)
 
1129
            if x - width/2 > next_x and x - width/2 > self.borders[HORZ]:
 
1130
                self.context.move_to(x - width/2, self.height - self.borders[VERT] + self.max_value[HORZ] + 3)
 
1131
                self.context.show_text(item)
 
1132
                next_x = x + width/2
1003
1133
            x += step
1004
 
 
 
1134
            
1005
1135
    def render_vert_labels(self):
1006
1136
        y = self.borders[VERT]
1007
1137
        step = (self.height - 2*self.borders[VERT])/(len(self.labels[VERT]) - 1)
1067
1197
                    self.context.fill()
1068
1198
                
1069
1199
                x0 += inner_step
1070
 
 
 
1200
    
1071
1201
class PiePlot(Plot):
1072
1202
    def __init__ (self,
1073
1203
            surface = None, 
1088
1218
 
1089
1219
    def load_series(self, data, x_labels=None, y_labels=None, series_colors=None):
1090
1220
        Plot.load_series(self, data, x_labels, y_labels, series_colors)
 
1221
        self.data = sorted(self.data)
1091
1222
 
1092
1223
    def draw_piece(self, angle, next_angle):
1093
1224
        self.context.move_to(self.center[0],self.center[1])
1128
1259
            angle = next_angle
1129
1260
 
1130
1261
    def render_plot(self):
1131
 
        angle = 0
 
1262
        angle = 3*math.pi/2.0
1132
1263
        next_angle = 0
1133
1264
        x0,y0 = self.center
1134
1265
        cr = self.context
1629
1760
    plot.render()
1630
1761
    plot.commit()
1631
1762
 
1632
 
def bar_plot(name, 
1633
 
             data, 
1634
 
             width, 
1635
 
             height, 
1636
 
             background = None, 
1637
 
             border = 0, 
1638
 
             grid = False,
1639
 
             rounded_corners = False,
1640
 
             three_dimension = False,
1641
 
             x_labels = None, 
1642
 
             y_labels = None, 
1643
 
             x_bounds = None, 
1644
 
             y_bounds = None,
1645
 
             colors = None):
1646
 
 
1647
 
    '''
1648
 
        - Function to generate Bar Plot Charts.
 
1763
def vertical_bar_plot(name, 
 
1764
                      data, 
 
1765
                      width, 
 
1766
                      height, 
 
1767
                      background = None, 
 
1768
                      border = 0, 
 
1769
                      grid = False,
 
1770
                      rounded_corners = False,
 
1771
                      three_dimension = False,
 
1772
                      x_labels = None, 
 
1773
                      y_labels = None, 
 
1774
                      x_bounds = None, 
 
1775
                      y_bounds = None,
 
1776
                      colors = None):
 
1777
    #TODO: Fix docstring for vertical_bar_plot
 
1778
    '''
 
1779
        - Function to generate vertical Bar Plot Charts.
 
1780
 
 
1781
        bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension, 
 
1782
                 x_labels, y_labels, x_bounds, y_bounds, colors):
 
1783
 
 
1784
        - Parameters
 
1785
        
 
1786
        name - Name of the desired output file, no need to input the .svg as it will be added at runtime;
 
1787
        data - The list, list of lists or dictionary holding the data to be plotted;
 
1788
        width, height - Dimensions of the output image;
 
1789
        background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. 
 
1790
                     If left None, a gray to white gradient will be generated;
 
1791
        border - Distance in pixels of a square border into which the graphics will be drawn;
 
1792
        grid - Whether or not the gris is to be drawn;
 
1793
        rounded_corners - Whether or not the bars should have rounded corners;
 
1794
        three_dimension - Whether or not the bars should be drawn in pseudo 3D;
 
1795
        x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis;
 
1796
        x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted;
 
1797
        colors - List containing the colors expected for each of the bars.
 
1798
 
 
1799
        - Example of use
 
1800
 
 
1801
        data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 
1802
        CairoPlot.vertical_bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False)
 
1803
    '''
 
1804
    
 
1805
    plot = VerticalBarPlot(name, data, width, height, background, border,
 
1806
                           grid, rounded_corners, three_dimension, x_labels, y_labels, x_bounds, y_bounds, colors)
 
1807
    plot.render()
 
1808
    plot.commit()
 
1809
 
 
1810
def horizontal_bar_plot(name, 
 
1811
                       data, 
 
1812
                       width, 
 
1813
                       height, 
 
1814
                       background = None, 
 
1815
                       border = 0, 
 
1816
                       grid = False,
 
1817
                       rounded_corners = False,
 
1818
                       three_dimension = False,
 
1819
                       x_labels = None, 
 
1820
                       y_labels = None, 
 
1821
                       x_bounds = None, 
 
1822
                       y_bounds = None,
 
1823
                       colors = None):
 
1824
 
 
1825
    #TODO: Fix docstring for horizontal_bar_plot
 
1826
    '''
 
1827
        - Function to generate Horizontal Bar Plot Charts.
1649
1828
 
1650
1829
        bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension, 
1651
1830
                 x_labels, y_labels, x_bounds, y_bounds, colors):
1671
1850
        CairoPlot.bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False)
1672
1851
    '''
1673
1852
    
1674
 
    plot = BarPlot(name, data, width, height, background, border,
1675
 
                   grid, rounded_corners, three_dimension, x_labels, y_labels, x_bounds, y_bounds, colors)
 
1853
    plot = HorizontalBarPlot(name, data, width, height, background, border,
 
1854
                             grid, rounded_corners, three_dimension, x_labels, y_labels, x_bounds, y_bounds, colors)
1676
1855
    plot.render()
1677
1856
    plot.commit()
1678
1857