1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
39 COLORS = color_list_from_file(os.path.dirname(__file__) + "/data/tango.color")
40
41
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=""):
64
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
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
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
105 """
106 Returns the current value of the PieArea.
107
108 @return: float.
109 """
110 return self.get_property("value")
111
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
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
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
141 """
142 Returns the current label of the area.
143
144 @return: string.
145 """
146 return self.get_property("label")
147
148
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
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
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
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
238
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
250
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
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
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
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
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
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
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
331 color = area.get_color()
332
333
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
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
400
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
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
427 """
428 Get the current rotation angle in degrees.
429
430 @return: integer.
431 """
432 return self.get_property("rotate")
433
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
444 """
445 Returns True if pie chart currently has a shadow.
446
447 @return: boolean.
448 """
449 return self.get_property("draw-shadow")
450
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
461 """
462 Returns True if area labels are shown.
463
464 @return: boolean.
465 """
466 return self.get_property("draw-labels")
467
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
478 """
479 Returns True if percentages are shown.
480
481 @return: boolean.
482 """
483 return self.get_property("show-percentage")
484
493
501
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
512 """
513 Returns True if the mouseover effect is enabled.
514
515 @return: boolean.
516 """
517 return self.get_property("enable-mouseover")
518
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
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