2
# -*- coding: utf-8 -*-
6
# Copyright (c) 2008 Rodrigo Moreira Araújo
8
# Author: Rodrigo Moreiro Araujo <alf.rodrigo@gmail.com>
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.
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.
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
25
#Contributor: João S. O. Bueno
36
def other_direction(direction):
37
"explicit is better than implicit"
53
self.create_surface(surface, width, height)
56
self.context = cairo.Context(self.surface)
57
self.load_series(data, h_labels, v_labels)
60
self.labels[HORZ] = h_labels
61
self.labels[VERT] = v_labels
65
self.set_background (background)
69
self.line_color = (0.5, 0.5, 0.5)
71
self.label_color = (0.0, 0.0, 0.0)
72
self.grid_color = (0.8, 0.8, 0.8)
75
def create_surface(self, surface, width=None, height=None):
77
if isinstance(surface, cairo.Surface):
78
self.surface = surface
80
if not type(surface) in (str, unicode):
81
raise TypeError("Surface should be either a Cairo surface or a filename, not %s" % surface)
82
sufix = surface.rsplit(".")[-1].lower()
83
self.filename = surface
85
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
87
self.surface = cairo.PSSurface(surface, width, height)
89
self.surface = cairo.PSSurface(surface, width, height)
92
self.filename += ".svg"
93
self.surface = cairo.SVGSurface(self.filename, width, height)
100
self.context.show_page()
101
if self.filename.endswith(".png"):
102
self.surface.write_to_png(self.filename)
104
self.surface.finish()
108
def load_series (self, data, h_labels=None, v_labels=None):
109
#FIXME: implement Series class for holding series data,
110
# labels and presentation properties
112
#data can be a list, a list of lists or a dictionary with
113
#each item as a labeled data series.
114
#we should (for teh time being) create a list of lists
115
#and set labels for teh series rom teh values provided.
117
self.series_labels = []
119
#if we have labeled series:
120
if hasattr(data, "keys"):
122
self.series_labels = data.keys()
123
for key in self.series_labels:
124
self.data.append(data[key])
125
#if we have a series of series:
126
elif hasattr(data[0], "__getitem__"):
128
self.series_labels = range(len(data))
131
self.series_labels = None
132
#FIXME: select some pre-sets and allow these to be parametrized:
134
self.series_colors = [[random.random() for i in range(3)] for series in self.data]
135
self.series_widths = [1.0 for series in self.data]
138
return self.surface.get_width()
139
def get_height(self):
140
return self.surface.get_height()
142
def set_background(self, background):
143
if background is None:
144
self.background = cairo.LinearGradient(self.width / 2, 0, self.width / 2, self.height)
145
self.background.add_color_stop_rgb(0,1.0,1.0,1.0)
146
self.background.add_color_stop_rgb(1.0,0.9,0.9,0.9)
148
if type(background) in (cairo.LinearGradient, tuple):
149
self.background = background
151
raise TypeError ("Background should be either cairo.LinearGradient or a 3-tuple, not %s" % type(background))
153
def render_background(self):
154
if isinstance (self.background, cairo.LinearGradient):
155
self.context.set_source(self.background)
157
self.context.set_source_rgb(*self.background)
158
self.context.rectangle(0,0, self.width, self.height)
161
def render_bounding_box(self):
162
self.context.set_source_rgb(*self.line_color)
163
self.context.set_line_width(self.line_width)
164
self.context.rectangle(self.border, self.border,
165
self.width - 2 * self.border,
166
self.height - 2 * self.border)
171
class DotLinePlot(Plot):
187
self.bounds[HORZ] = h_bounds
188
self.bounds[VERT] = v_bounds
190
Plot.__init__(self, surface, data, width, height, background, border, h_labels, v_labels)
196
self.h_label_angle = math.pi / 2.5
198
def load_series(self, data, h_labels = None, v_labels = None):
199
Plot.load_series(self, data, h_labels, v_labels)
200
self.calc_boundaries()
202
def calc_boundaries(self):
204
if not self.bounds[HORZ]:
205
self.bounds[HORZ] = (0, max([len(series) for series in (self.data)]))
207
if not self.bounds[VERT]:
208
max_data_value = min_data_value = 0
209
for series in self.data:
210
if max(series) > max_data_value:
211
max_data_value = max(series)
212
if min(series) < min_data_value:
213
min_data_value = min(series)
214
self.bounds[VERT] = (min_data_value, max_data_value)
217
def calc_extents(self, direction):
219
self.max_value[direction] = 0
220
if self.labels[direction]:
221
widest_word = max(self.labels[direction], lambda item: self.context.text_extents(item)[2])
222
self.max_value[direction] = self.context.text_extents(widest_word)[2]
223
self.borders[other_direction(direction)] = self.max_value[direction] + self.border
225
self.max_value[direction] = self.context.text_extents(str(self.bounds[direction][1]))[2]
226
self.borders[other_direction(direction)] = self.max_value[direction] + self.border + 20
228
def calc_horz_extents(self):
229
self.calc_extents(HORZ)
231
def calc_vert_extents(self):
232
self.calc_extents(VERT)
235
def render_axis(self):
237
h_border = self.borders[HORZ]
238
v_border = self.borders[VERT]
239
cr.set_source_rgb(*self.line_color)
241
cr.move_to(h_border, self.height - v_border)
242
cr.line_to(h_border, v_border)
245
cr.move_to(h_border, self.height - v_border)
246
cr.line_to(self.width - h_border, self.height - v_border)
249
def render_labels(self):
251
self.context.set_font_size(self.font_size * 0.8)
253
self.render_horz_labels()
254
self.render_vert_labels()
256
def render_horz_labels(self):
258
labels = self.labels[HORZ]
260
labels = [str(i) for i in range(self.bounds[HORZ][0], self.bounds[HORZ][1])]
261
border = self.borders[HORZ]
263
step = (self.width - 2 * border) / len(labels)
266
cr.set_source_rgb(*self.label_color)
267
width = cr.text_extents(item)[2]
268
cr.move_to(x, self.height - self.borders[VERT] + 10)
269
cr.rotate(self.h_label_angle)
271
cr.rotate(-self.h_label_angle)
272
#FIXME: render grid in a separate method
273
if self.grid and x != border:
274
cr.set_source_rgb(*self.grid_color)
275
cr.move_to(x, self.height - self.borders[VERT])
276
cr.line_to(x, self.borders[VERT])
280
def render_vert_labels(self):
282
labels = self.labels[VERT]
284
amplitude = self.bounds[VERT][1] - self.bounds[VERT][0]
285
#if vertical labels need decimal points
287
label_type = lambda x : int(x)
289
label_type = lambda x: float(x)
292
self.bounds[VERT][0] +
293
(amplitude * i / 10.0)
295
) for i in range(10) ]
296
border = self.borders[VERT]
298
step = (self.height - 2 * border)/ len(labels)
299
y = self.height - border
301
cr.set_source_rgb(*self.label_color)
302
width = cr.text_extents(item)[2]
303
cr.move_to(self.borders[HORZ] - width - 5,y)
305
#FIXME: render grid in a separate method
306
if self.grid and y != self.height - border:
307
cr.set_source_rgb(*self.grid_color)
308
cr.move_to(self.borders[HORZ], y)
309
cr.line_to(self.width - self.borders[HORZ], y)
315
self.calc_horz_extents()
316
self.calc_vert_extents()
318
self.render_background()
319
self.render_bounding_box()
328
def render_series_labels(self):
329
#FIXME: implement this
330
for key in self.series_labels:
332
#This was not working in Rodrigo's original code anyway
334
def render_plot(self):
335
#render_series_labels
336
largest_series_length = len(max(self.data, key=len))
337
#FIXME: plot_width and plot_height should be object properties and be re-used.
338
plot_width = self.width - 2* self.borders[HORZ]
339
plot_height = self.height - 2 * self.borders[VERT]
340
plot_top = self.height - self.borders[VERT]
342
series_amplitude = self.bounds[VERT][1] - self.bounds[VERT][0]
344
horizontal_step = float (plot_width) / largest_series_length
345
vertical_step = float (plot_height) / series_amplitude
348
for number, series in enumerate (self.data):
349
cr.set_source_rgb(*self.series_colors[number])
350
x = self.borders[HORZ]
352
#FIXME: separate plotting of lines, dots and area
356
cr.move_to(x - horizontal_step, plot_top - int(last * vertical_step))
357
cr.line_to(x, plot_top - int(value * vertical_step))
358
cr.set_line_width(self.series_widths[number])
361
cr.arc(x, plot_top - int(value * vertical_step), 3, 0, 2.1 * math.pi)
370
def dot_line_plot(name,
383
Function to plot graphics using dots and lines.
384
dot_line_plot (name, data, width, height, background = None, border = 0, axis = False, grid = False, h_legend = None, v_legend = None, h_bounds = None, v_bounds = None)
388
name - Name of the desired output file, no need to input the .svg as it will be added at runtim;
389
data - The list, list of lists or dictionary holding the data to be plotted;
390
width, height - Dimensions of the output image;
391
background - A 3 element tuple representing the rgb color expected for the background. If left None, a gray to white gradient will be generated;
392
border - Distance in pixels of a square border into which the graphics will be drawn;
393
axis - Whether or not the axis are to be drawn;
394
grid - Whether or not the gris is to be drawn;
395
h_legend, v_legend - lists of strings containing the horizontal and vertical legends for the axis;
396
h_bounds, v_bounds - tuples containing the lower and upper value bounds for the data to be plotted.
400
teste_data = [0, 1, 3, 8, 9, 0, 10, 10, 2, 1]
401
CairoPlot.dot_line_plot('teste', teste_data, 400, 300)
403
teste_data_2 = {"john" : [10, 10, 10, 10, 30], "mary" : [0, 0, 3, 5, 15], "philip" : [13, 32, 11, 25, 2]}
404
teste_h_legend = ["jan/2008", "feb/2008", "mar/2008", "apr/2008", "may/2008"]
405
CairoPlot.dot_line_plot('teste2', teste_data_2, 400, 300, axis = True, grid = True, h_legend = teste_h_legend)
407
plot = DotLinePlot(name, data, width, height, background, border,
408
axis, grid, h_legend, v_legend, h_bounds, v_bounds)
414
def pizza_plot(name, data, width, height, background = None):
417
Function to plot pizza graphics.
418
pizza_plot(name, data, width, height, background = None)
422
name - Name of the desired output file, no need to input the .svg as it will be added at runtim;
423
data - The list, list of lists or dictionary holding the data to be plotted;
424
width, height - Dimensions of the output image;
425
background - A 3 element tuple representing the rgb color expected for the background. If left None, a gray to white gradient will be generated;
429
teste_data = {"john" : 123, "mary" : 489, "philip" : 890 , "suzy" : 235}
430
CairoPlot.pizza_plot("pizza_teste", teste_data, 500, 500)
434
surface = cairo.SVGSurface(name + '.svg', width, height)
436
cr = cairo.Context(surface)
438
if background != None:
439
cr.set_source_rgb(background[0], background[1], background[2])
440
cr.rectangle(0,0,width,height)
443
linear = cairo.LinearGradient(width/2, 0, width/2, height)
444
linear.add_color_stop_rgb(0,1.0,1.0,1.0)
445
linear.add_color_stop_rgb(1.0,0.9,0.9,0.9)
446
cr.set_source(linear)
447
cr.rectangle(0,0, width, height)
452
cr.set_line_width(2.0)
456
for key in data.keys():
459
for key in data.keys():
460
next_angle = angle + 2.0*math.pi*data[key]/n
462
radius = width/3 if width < height else height/3
464
cr.set_source_rgb(random.random(), random.random(), random.random())
466
w = cr.text_extents(key)[2]
467
if (angle + next_angle)/2 < math.pi/2 or (angle + next_angle)/2 > 3*math.pi/2:
468
cr.move_to(x0 + (radius+10)*math.cos((angle+next_angle)/2), y0 + (radius+10)*math.sin((angle+next_angle)/2) )
470
cr.move_to(x0 + (radius+10)*math.cos((angle+next_angle)/2) - w, y0 + (radius+10)*math.sin((angle+next_angle)/2) )
474
cr.line_to(x0+radius*math.cos(angle), y0+radius*math.sin(angle))
475
cr.arc(x0, y0, radius, angle, angle + 2.0*math.pi*data[key]/n)
479
cr.set_source_rgb(1.0, 1.0, 1.0)
481
cr.line_to(x0+radius*math.cos(angle), y0+radius*math.sin(angle))
482
cr.arc(x0, y0, radius, angle, angle + 2.0*math.pi*data[key]/n)
489
def drawRectangle(cr, x0, y0, x1, y1, color):
491
linear = cairo.LinearGradient(mid,y0,mid,y1)
492
linear.add_color_stop_rgb(0,3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0)
493
linear.add_color_stop_rgb(1,color[0],color[1],color[2])
494
cr.set_source(linear)
496
cr.arc(x0+5, y0+5, 5, 0, 2*math.pi)
497
cr.arc(x1-5, y0+5, 5, 0, 2*math.pi)
498
cr.arc(x0+5, y1-5, 5, 0, 2*math.pi)
499
cr.arc(x1-5, y1-5, 5, 0, 2*math.pi)
500
cr.rectangle(x0+5,y0,x1-x0-10,y1-y0)
501
cr.rectangle(x0,y0+5,x1-x0,y1-y0-10)
504
def drawShadow(cr, x0, y0, x1, y1):
508
h_linear_1 = cairo.LinearGradient(h_mid,y0-4,h_mid,y0+4)
509
h_linear_2 = cairo.LinearGradient(h_mid,y1-4,h_mid,y1+4)
510
v_linear_1 = cairo.LinearGradient(x0-4,v_mid,x0+4,v_mid)
511
v_linear_2 = cairo.LinearGradient(x1-4,v_mid,x1+4,v_mid)
512
radial_00 = cairo.RadialGradient(x0+4, y0+4, 0, x0+4, y0+4, 8)
513
radial_01 = cairo.RadialGradient(x1-4, y0+4, 0, x1-4, y0+4, 8)
514
radial_10 = cairo.RadialGradient(x0+4, y1-4, 0, x0+4, y1-4, 8)
515
radial_11 = cairo.RadialGradient(x1-4, y1-4, 0, x1-4, y1-4, 8)
518
h_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0)
519
h_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow)
520
h_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow)
521
h_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0)
522
v_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0)
523
v_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow)
524
v_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow)
525
v_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0)
526
cr.set_source(h_linear_1)
527
#cr.set_source_rgb(0,0,1)
528
cr.rectangle(x0+4,y0-4,x1-x0-8,8)
530
cr.set_source(h_linear_2)
531
#cr.set_source_rgb(0,0,1)
532
cr.rectangle(x0+4,y1-4,x1-x0-8,8)
534
cr.set_source(v_linear_1)
535
#cr.set_source_rgb(0,0,1)
536
cr.rectangle(x0-4,y0+4,8,y1-y0-8)
538
cr.set_source(v_linear_2)
539
#cr.set_source_rgb(0,0,1)
540
cr.rectangle(x1-4,y0+4,8,y1-y0-8)
543
radial_00.add_color_stop_rgba(0, 0, 0, 0, shadow)
544
radial_00.add_color_stop_rgba(1, 0, 0, 0, 0)
545
radial_01.add_color_stop_rgba(0, 0, 0, 0, shadow)
546
radial_01.add_color_stop_rgba(1, 0, 0, 0, 0)
547
radial_10.add_color_stop_rgba(0, 0, 0, 0, shadow)
548
radial_10.add_color_stop_rgba(1, 0, 0, 0, 0)
549
radial_11.add_color_stop_rgba(0, 0, 0, 0, shadow)
550
radial_11.add_color_stop_rgba(1, 0, 0, 0, 0)
551
#cr.set_source_rgb(0,0,1)
552
cr.set_source(radial_00)
553
cr.move_to(x0+4,y0+4)
555
cr.arc(x0+4, y0+4, 8, math.pi, 3*math.pi/2)
556
cr.line_to(x0+4,y0+4)
559
#cr.set_source_rgb(0,0,1)
560
cr.set_source(radial_01)
561
cr.move_to(x1-4,y0+4)
563
cr.arc(x1-4, y0+4, 8, 3*math.pi/2, 2*math.pi)
564
cr.line_to(x1-4,y0+4)
567
#cr.set_source_rgb(0,0,0)
568
cr.set_source(radial_10)
569
cr.move_to(x0+4,y1-4)
571
cr.arc(x0+4, y1-4, 8, math.pi/2, math.pi)
572
cr.line_to(x0+4,y1-4)
575
#cr.set_source_rgb(0,0,0)
576
cr.set_source(radial_11)
577
cr.move_to(x1-4,y1-4)
579
cr.arc(x1-4, y1-4, 8, 0, math.pi/2)
580
cr.line_to(x1-4,y1-4)
584
def gantt_chart(name, pieces, width, height, h_legend, v_legend, colors):
587
Function to generate Gantt Diagrams.
588
gantt_chart(name, pieces, width, height, h_legend, v_legend, colors):
592
name - Name of the desired output file, no need to input the .svg as it will be added at runtim;
593
pieces - A list defining the spaces to be drawn. The user must pass, for each line, the index of its start and the index of its end. If a line must have two or more spaces, they must be passed inside a list;
594
width, height - Dimensions of the output image;
595
h_legend - A list of names for each of the vertical lines;
596
v_legend - A list of names for each of the horizontal spaces;
597
colors - List containing the colors expected for each of the horizontal spaces
601
pieces = [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,8)]
602
h_legend = [ 'teste01', 'teste02', 'teste03', 'teste04']
603
v_legend = [ '0001', '0002', '0003', '0004', '0005', '0006', '0007', '0008', '0009', '0010' ]
604
colors = [ (1.0, 0.0, 0.0), (1.0, 0.7, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0) ]
605
CairoPlot.gantt_chart('gantt_teste', pieces, 600, 300, h_legend, v_legend, colors)
609
surface = cairo.SVGSurface(name + '.svg', width, height)
610
cr = cairo.Context(surface)
611
cr.set_source_rgb(1.0, 1.0, 1.0)
612
cr.rectangle(0,0,width,height)
614
cr.set_font_size(0.02*width)
616
for word in h_legend:
617
max_word = word if word != None and len(word) > len(max_word) else max_word
618
h_border = 100 + cr.text_extents(max_word)[2]
619
horizontal_step = (width-h_border)/len(v_legend)
620
vertical_step = height/(len(h_legend) + 1)
621
v_border = vertical_step
624
linear = cairo.LinearGradient(width/2, v_border + pieces.index(line)*vertical_step, width/2, v_border + (pieces.index(line) + 1)*vertical_step)
625
linear.add_color_stop_rgb(0,1.0,1.0,1.0)
626
linear.add_color_stop_rgb(1.0,0.9,0.9,0.9)
627
cr.set_source(linear)
628
cr.rectangle(0,v_border + pieces.index(line)*vertical_step,width,vertical_step)
631
cr.set_font_size(0.015*width)
632
cr.set_source_rgb(0.7, 0.7, 0.7)
633
cr.set_dash((1,0,0,0,0,0,1))
634
cr.set_line_width(0.5)
635
for word in v_legend:
636
w,h = cr.text_extents(word)[2], cr.text_extents(word)[3]
637
cr.move_to(h_border + v_legend.index(word)*horizontal_step-w/2, vertical_step/2)
639
cr.move_to(h_border + v_legend.index(word)*horizontal_step, vertical_step/2 + h)
640
cr.line_to(h_border + v_legend.index(word)*horizontal_step, height)
643
cr.set_font_size(0.02*width)
645
word = h_legend[pieces.index(line)]
647
cr.set_source_rgb(0.5, 0.5, 0.5)
648
w,h = cr.text_extents(word)[2], cr.text_extents(word)[3]
649
cr.move_to(40,v_border + pieces.index(line)*vertical_step + vertical_step/2 + h/2)
652
if type(line) == type([]):
654
drawShadow(cr, h_border + space[0]*horizontal_step, v_border + pieces.index(line)*vertical_step + vertical_step/4.0,
655
h_border + space[1]*horizontal_step, v_border + pieces.index(line)*vertical_step + 3.0*vertical_step/4.0)
656
drawRectangle(cr, h_border + space[0]*horizontal_step, v_border + pieces.index(line)*vertical_step + vertical_step/4.0,
657
h_border + space[1]*horizontal_step, v_border + pieces.index(line)*vertical_step + 3.0*vertical_step/4.0, colors[pieces.index(line)])
660
drawShadow(cr, h_border + space[0]*horizontal_step, v_border + pieces.index(line)*vertical_step + vertical_step/4.0,
661
h_border + space[1]*horizontal_step, v_border + pieces.index(line)*vertical_step + 3.0*vertical_step/4.0)
662
drawRectangle(cr, h_border + space[0]*horizontal_step, v_border + pieces.index(line)*vertical_step + vertical_step/4.0,
663
h_border + space[1]*horizontal_step, v_border + pieces.index(line)*vertical_step + 3.0*vertical_step/4.0, colors[pieces.index(line)])