Package pygtk_chart :: Module pie_chart
[hide private]
[frames] | no frames]

Source Code for Module pygtk_chart.pie_chart

  1  #!/usr/bin/env python 
  2  # 
  3  #       pie_chart.py 
  4  #        
  5  #       Copyright 2008 Sven Festersen <sven@sven-festersen.de> 
  6  #        
  7  #       This program is free software; you can redistribute it and/or modify 
  8  #       it under the terms of the GNU General Public License as published by 
  9  #       the Free Software Foundation; either version 2 of the License, or 
 10  #       (at your option) any later version. 
 11  #        
 12  #       This program is distributed in the hope that it will be useful, 
 13  #       but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  #       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 15  #       GNU General Public License for more details. 
 16  #        
 17  #       You should have received a copy of the GNU General Public License 
 18  #       along with this program; if not, write to the Free Software 
 19  #       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
 20  #       MA 02110-1301, USA. 
 21  """ 
 22  Contains the PieChart widget. 
 23   
 24  Author: Sven Festersen (sven@sven-festersen.de) 
 25  """ 
 26  __docformat__ = "epytext" 
 27  import cairo 
 28  import gobject 
 29  import gtk 
 30  import math 
 31  import os 
 32   
 33  from pygtk_chart.basics import * 
 34  from pygtk_chart import chart 
 35   
 36  COLOR_AUTO = 0 
 37   
 38  #load default color palette 
 39  COLORS = color_list_from_file(os.path.dirname(__file__) + "/data/tango.color") 
 40   
 41   
42 -class PieArea(chart.ChartObject):
43 44 __gproperties__ = {"name": (gobject.TYPE_STRING, "pie are name", 45 "A unique name for the pie area.", 46 "", gobject.PARAM_READABLE), 47 "value": (gobject.TYPE_FLOAT, 48 "value", 49 "The value.", 50 0.0, 9999999999.0, 0.0, gobject.PARAM_READWRITE), 51 "color": (gobject.TYPE_PYOBJECT, "pie area color", 52 "The color of the area.", 53 gobject.PARAM_READWRITE), 54 "label": (gobject.TYPE_STRING, "area label", 55 "The label for the area.", "", 56 gobject.PARAM_READWRITE)} 57
58 - def __init__(self, name, value, label=""):
59 chart.ChartObject.__init__(self) 60 self._name = name 61 self._value = value 62 self._label = label 63 self._color = COLOR_AUTO
64
65 - def do_get_property(self, property):
66 if property.name == "visible": 67 return self._show 68 elif property.name == "antialias": 69 return self._antialias 70 elif property.name == "name": 71 return self._name 72 elif property.name == "value": 73 return self._value 74 elif property.name == "color": 75 return self._color 76 elif property.name == "label": 77 return self._label 78 else: 79 raise AttributeError, "Property %s does not exist." % property.name
80
81 - def do_set_property(self, property, value):
82 if property.name == "visible": 83 self._show = value 84 elif property.name == "antialias": 85 self._antialias = value 86 elif property.name == "value": 87 self._value = value 88 elif property.name == "color": 89 self._color = value 90 elif property.name == "label": 91 self._label = value 92 else: 93 raise AttributeError, "Property %s does not exist." % property.name
94
95 - def set_value(self, value):
96 """ 97 Set the value of the PieArea. 98 99 @type value: float. 100 """ 101 self.set_property("value", value) 102 self.emit("appearance_changed")
103
104 - def get_value(self):
105 """ 106 Returns the current value of the PieArea. 107 108 @return: float. 109 """ 110 return self.get_property("value")
111
112 - def set_color(self, color):
113 """ 114 Set the color of the pie area. Color has to either COLOR_AUTO or 115 a tuple (r, g, b) with r, g, b in [0, 1]. 116 117 @type color: a color. 118 """ 119 self.set_property("color", color) 120 self.emit("appearance_changed")
121
122 - def get_color(self):
123 """ 124 Returns the current color of the pie area or COLOR_AUTO. 125 126 @return: a color. 127 """ 128 return self.get_property("color")
129
130 - def set_label(self, label):
131 """ 132 Set the label for the pie chart area. 133 134 @param label: the new label 135 @type label: string. 136 """ 137 self.set_property("label", label) 138 self.emit("appearance_changed")
139
140 - def get_label(self):
141 """ 142 Returns the current label of the area. 143 144 @return: string. 145 """ 146 return self.get_property("label")
147 148
149 -class PieChart(chart.Chart):
150 151 __gproperties__ = {"rotate": (gobject.TYPE_INT, 152 "rotation", 153 "The angle to rotate the chart in degrees.", 154 0, 360, 0, gobject.PARAM_READWRITE), 155 "draw-shadow": (gobject.TYPE_BOOLEAN, 156 "draw pie shadow", 157 "Set whether to draw pie shadow.", 158 True, gobject.PARAM_READWRITE), 159 "draw-labels": (gobject.TYPE_BOOLEAN, 160 "draw area labels", 161 "Set whether to draw area labels.", 162 True, gobject.PARAM_READWRITE), 163 "show-percentage": (gobject.TYPE_BOOLEAN, 164 "show percentage", 165 "Set whether to show percentage in the areas' labels.", 166 False, gobject.PARAM_READWRITE), 167 "show-values": (gobject.TYPE_BOOLEAN, 168 "show values", 169 "Set whether to show values in the areas' labels.", 170 True, gobject.PARAM_READWRITE), 171 "enable-scroll": (gobject.TYPE_BOOLEAN, 172 "enable scroll", 173 "If True, the pie can be rotated by scrolling with the mouse wheel.", 174 True, gobject.PARAM_READWRITE), 175 "enable-mouseover": (gobject.TYPE_BOOLEAN, 176 "enable mouseover", 177 "Set whether a mouseover effect should be visible if moving the mouse over a pie area.", 178 True, gobject.PARAM_READWRITE)} 179 180 __gsignals__ = {"area-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))} 181
182 - def __init__(self):
183 chart.Chart.__init__(self) 184 self._areas = [] 185 self._rotate = 0 186 self._shadow = True 187 self._labels = True 188 self._percentage = False 189 self._values = True 190 self._enable_scroll = True 191 self._enable_mouseover = True 192 193 self._highlighted = None 194 195 self.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.SCROLL_MASK|gtk.gdk.POINTER_MOTION_MASK) 196 self.connect("button_press_event", self._cb_button_pressed) 197 self.connect("scroll-event", self._cb_scroll_event) 198 self.connect("motion-notify-event", self._cb_motion_notify)
199
200 - def do_get_property(self, property):
201 if property.name == "rotate": 202 return self._rotate 203 elif property.name == "draw-shadow": 204 return self._shadow 205 elif property.name == "draw-labels": 206 return self._labels 207 elif property.name == "show-percentage": 208 return self._percentage 209 elif property.name == "show-values": 210 return self._values 211 elif property.name == "enable-scroll": 212 return self._enable_scroll 213 elif property.name == "enable-mouseover": 214 return self._enable_mouseover 215 else: 216 raise AttributeError, "Property %s does not exist." % property.name
217
218 - def do_set_property(self, property, value):
219 if property.name == "rotate": 220 self._rotate = value 221 elif property.name == "draw-shadow": 222 self._shadow = value 223 elif property.name == "draw-labels": 224 self._labels = value 225 elif property.name == "show-percentage": 226 self._percentage = value 227 elif property.name == "show-values": 228 self._values = value 229 elif property.name == "enable-scroll": 230 self._enable_scroll = value 231 elif property.name == "enable-mouseover": 232 self._enable_mouseover = value 233 else: 234 raise AttributeError, "Property %s does not exist." % property.name
235
236 - def _cb_appearance_changed(self, widget):
237 self.queue_draw()
238
239 - def _cb_motion_notify(self, widget, event):
240 if not self._enable_mouseover: return 241 area = self._get_area_at_pos(event.x, event.y) 242 if area != self._highlighted: 243 self.queue_draw() 244 self._highlighted = area
245
246 - def _cb_button_pressed(self, widget, event):
247 area = self._get_area_at_pos(event.x, event.y) 248 if area: 249 self.emit("area-clicked", area)
250
251 - def _get_area_at_pos(self, x, y):
252 rect = self.get_allocation() 253 center = rect.width / 2, rect.height / 2 254 x = x - center[0] 255 y = y - center[1] 256 257 #calculate angle 258 angle = math.atan2(x, -y) 259 angle -= math.pi / 2 260 angle -= 2 * math.pi * self.get_rotate() / 360.0 261 while angle < 0: 262 angle += 2 * math.pi 263 264 #calculate radius 265 radius_squared = math.pow(int(0.4 * min(rect.width, rect.height)), 2) 266 clicked_radius_squared = x*x + y*y 267 268 if clicked_radius_squared <= radius_squared: 269 #find out area that was clicked 270 sum = 0 271 for area in self._areas: 272 if area.get_visible(): 273 sum += area.get_value() 274 275 current_angle_position = 0 276 for area in self._areas: 277 area_angle = 2 * math.pi * area.get_value() / sum 278 279 if current_angle_position <= angle <= current_angle_position + area_angle: 280 return area 281 282 current_angle_position += area_angle 283 return None
284
285 - def _cb_scroll_event(self, widget, event):
286 if not self._enable_scroll: return 287 if event.direction in [gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_RIGHT]: 288 delta = 360.0 / 32 289 elif event.direction in [gtk.gdk.SCROLL_DOWN, gtk.gdk.SCROLL_LEFT]: 290 delta = - 360.0 / 32 291 else: 292 delta = 0 293 rotate = self.get_rotate() + delta 294 rotate = rotate % 360.0 295 if rotate < 0: rotate += 360 296 self.set_rotate(rotate)
297
298 - def draw(self, context):
299 """ 300 Draw the widget. This method is called automatically. Don't call it 301 yourself. If you want to force a redrawing of the widget, call 302 the queue_draw() method. 303 304 @type context: cairo.Context 305 @param context: The context to draw on. 306 """ 307 rect = self.get_allocation() 308 #initial context settings: line width & font 309 context.set_line_width(1) 310 font = gtk.Label().style.font_desc.get_family() 311 context.select_font_face(font, cairo.FONT_SLANT_NORMAL, \ 312 cairo.FONT_WEIGHT_NORMAL) 313 314 self.draw_basics(context, rect) 315 self._do_draw_shadow(context, rect) 316 self._do_draw_areas(context, rect)
317
318 - def _do_draw_areas(self, context, rect):
319 center = rect.width / 2, rect.height / 2 320 radius = int(0.4 * min(rect.width, rect.height)) 321 sum = 0 322 323 for area in self._areas: 324 if area.get_visible(): 325 sum += area.get_value() 326 327 current_angle_position = 2 * math.pi * self.get_rotate() / 360.0 328 for i, area in enumerate(self._areas): 329 if not area.get_visible(): continue 330 #set the color or automaticly select one: 331 color = area.get_color() 332 #if color == COLOR_AUTO: color = COLORS[i % len(COLORS)] 333 #draw the area: 334 area_angle = 2 * math.pi * area.get_value() / sum 335 context.set_source_rgb(*color) 336 context.move_to(center[0], center[1]) 337 context.arc(center[0], center[1], radius, current_angle_position, current_angle_position + area_angle) 338 context.close_path() 339 context.fill() 340 341 if area == self._highlighted: 342 context.set_source_rgba(1, 1, 1, 0.1) 343 context.move_to(center[0], center[1]) 344 context.arc(center[0], center[1], radius, current_angle_position, current_angle_position + area_angle) 345 context.close_path() 346 context.fill() 347 348 if self._labels: 349 font_name = gtk.Label().style.font_desc.get_family() 350 font_slant = cairo.FONT_SLANT_NORMAL 351 if area == self._highlighted: 352 font_slant = cairo.FONT_SLANT_ITALIC 353 context.set_source_rgb(*color) 354 355 label = area.get_label() 356 label_extra = "" 357 if self._percentage and not self._values: 358 label_extra = " (%s%%)" % round(100. * area.get_value() / sum, 2) 359 elif not self._percentage and self._values: 360 label_extra = " (%s)" % area.get_value() 361 elif self._percentage and self._values: 362 label_extra = " (%s, %s%%)" % (area.get_value(), round(100. * area.get_value() / sum, 2)) 363 label += label_extra 364 angle = current_angle_position + area_angle / 2 365 angle = angle % (2 * math.pi) 366 x = center[0] + (radius + 10) * math.cos(angle) 367 y = center[1] + (radius + 10) * math.sin(angle) 368 369 ref = REF_BOTTOM_LEFT 370 if 0 <= angle <= math.pi / 2: 371 ref = REF_TOP_LEFT 372 elif math.pi / 2 <= angle <= math.pi: 373 ref = REF_TOP_RIGHT 374 elif math.pi <= angle <= 1.5 * math.pi: 375 ref = REF_BOTTOM_RIGHT 376 377 show_text(context, rect, x, y, label, font_name, rect.height / 30, slant=font_slant, reference_point=ref) 378 context.fill() 379 380 current_angle_position += area_angle
381
382 - def _do_draw_shadow(self, context, rect):
383 if not self._shadow: return 384 center = rect.width / 2, rect.height / 2 385 radius = int(0.4 * min(rect.width, rect.height)) 386 387 gradient = cairo.RadialGradient(center[0], center[1], radius, center[0], center[1], radius + 10) 388 gradient.add_color_stop_rgba(0, 0, 0, 0, 0.5) 389 gradient.add_color_stop_rgba(0.5, 0, 0, 0, 0) 390 391 context.set_source(gradient) 392 context.arc(center[0], center[1], radius + 10, 0, 2 * math.pi) 393 context.fill()
394
395 - def add_area(self, area):
396 color = area.get_color() 397 if color == COLOR_AUTO: area.set_color(COLORS[len(self._areas) % len(COLORS)]) 398 self._areas.append(area) 399 area.connect("appearance_changed", self._cb_appearance_changed)
400
401 - def get_pie_area(self, name):
402 """ 403 Returns the PieArea with the id 'name' if it exists, None 404 otherwise. 405 406 @type name: string 407 @param name: the id of a PieArea 408 409 @return: a PieArea or None. 410 """ 411 for area in self._areas: 412 if area.get_name() == name: 413 return area 414 return None
415
416 - def set_rotate(self, angle):
417 """ 418 Set the rotation angle of the PieChart in degrees. 419 420 @param angle: angle in degrees 0 - 360 421 @type angle: integer. 422 """ 423 self.set_property("rotate", angle) 424 self.queue_draw()
425
426 - def get_rotate(self):
427 """ 428 Get the current rotation angle in degrees. 429 430 @return: integer. 431 """ 432 return self.get_property("rotate")
433
434 - def set_draw_shadow(self, draw):
435 """ 436 Set whether to draw the pie chart's shadow. 437 438 @type draw: boolean. 439 """ 440 self.set_property("draw-shadow", draw) 441 self.queue_draw()
442
443 - def get_draw_shadow(self):
444 """ 445 Returns True if pie chart currently has a shadow. 446 447 @return: boolean. 448 """ 449 return self.get_property("draw-shadow")
450
451 - def set_draw_labels(self, draw):
452 """ 453 Set whether to draw the labels of the pie areas. 454 455 @type draw: boolean. 456 """ 457 self.set_property("draw-labels", draw) 458 self.queue_draw()
459
460 - def get_draw_labels(self):
461 """ 462 Returns True if area labels are shown. 463 464 @return: boolean. 465 """ 466 return self.get_property("draw-labels")
467
468 - def set_show_percentage(self, show):
469 """ 470 Set whether to show the percentage an area has in its label. 471 472 @type show: boolean. 473 """ 474 self.set_property("show-percentage", show) 475 self.queue_draw()
476
477 - def get_show_percentage(self):
478 """ 479 Returns True if percentages are shown. 480 481 @return: boolean. 482 """ 483 return self.get_property("show-percentage")
484
485 - def set_enable_scroll(self, scroll):
486 """ 487 Set whether the pie chart can be rotated by scrolling with 488 the mouse wheel. 489 490 @type scroll: boolean. 491 """ 492 self.set_property("enable-scroll", scroll)
493
494 - def get_enable_scroll(self):
495 """ 496 Returns True if the user can rotate the pie chart by scrolling. 497 498 @return: boolean. 499 """ 500 return self.get_property("enable-scroll")
501
502 - def set_enable_mouseover(self, mouseover):
503 """ 504 Set whether a mouseover effect should be shown when the pointer 505 enters a pie area. 506 507 @type mouseover: boolean. 508 """ 509 self.set_property("enable-mouseover", mouseover)
510
511 - def get_enable_mouseover(self):
512 """ 513 Returns True if the mouseover effect is enabled. 514 515 @return: boolean. 516 """ 517 return self.get_property("enable-mouseover")
518
519 - def set_show_values(self, show):
520 """ 521 Set whether the area's value should be shown in its label. 522 523 @type show: boolean. 524 """ 525 self.set_property("show-values", show) 526 self.queue_draw()
527
528 - def get_show_values(self):
529 """ 530 Returns True if the value of a pie area is shown in its label. 531 532 @return: boolean. 533 """ 534 return self.get_property("show-values")
535