1 """
2 Contains the BarChart widget.
3
4 Author: John Dickinson (john@johnandkaren.com)
5 """
6 __docformat__ = "epytext"
7 import cairo
8 import gtk
9 import gobject
10 import os
11 import collections
12 import math
13
14 from pygtk_chart.basics import *
15 from pygtk_chart import chart
16
17 COLOR_AUTO = 0
18
19 COLORS = color_list_from_file(os.path.dirname(__file__) + "/data/tango.color")
20
21 -class Bar(chart.ChartObject):
22 __gproperties__ = {"name": (gobject.TYPE_STRING, "bar name",
23 "A unique name for the bar.",
24 "", gobject.PARAM_READABLE),
25 "value": (gobject.TYPE_FLOAT,
26 "value",
27 "The value.",
28 0.0, 9999999999.0, 0.0, gobject.PARAM_READWRITE),
29 "color": (gobject.TYPE_PYOBJECT, "bar color",
30 "The color of the bar.",
31 gobject.PARAM_READWRITE),
32 "label": (gobject.TYPE_STRING, "bar label",
33 "The label for the bar.", "",
34 gobject.PARAM_READWRITE)}
35
36 - def __init__(self, name, value, label=""):
37 super(Bar, self).__init__()
38 self._name = name
39 self._value = value
40 self._label = label
41 self._color = COLOR_AUTO
42
44 if property.name == "visible":
45 return self._show
46 elif property.name == "antialias":
47 return self._antialias
48 elif property.name == "name":
49 return self._name
50 elif property.name == "value":
51 return self._value
52 elif property.name == "color":
53 return self._color
54 elif property.name == "label":
55 return self._label
56 else:
57 raise AttributeError, "Property %s does not exist." % property.name
58
60 if property.name == "visible":
61 self._show = value
62 elif property.name == "antialias":
63 self._antialias = value
64 elif property.name == "value":
65 self._value = value
66 elif property.name == "color":
67 self._color = value
68 elif property.name == "label":
69 self._label = value
70 else:
71 raise AttributeError, "Property %s does not exist." % property.name
72
74 """
75 Set the value of the Bar.
76
77 @type value: float.
78 """
79 self.set_property("value", value)
80 self.emit("appearance_changed")
81
83 """
84 Returns the current value of the Bar.
85
86 @return: float.
87 """
88 return self.get_property("value")
89
91 """
92 Set the color of the bar. Color has to either COLOR_AUTO or
93 a tuple (r, g, b) with r, g, b in [0, 1].
94
95 @type color: a color.
96 """
97 self.set_property("color", color)
98 self.emit("appearance_changed")
99
101 """
102 Returns the current color of the bar or COLOR_AUTO.
103
104 @return: a color.
105 """
106 return self.get_property("color")
107
109 """
110 Set the label for the bar chart bar.
111
112 @param label: the new label
113 @type label: string.
114 """
115 self.set_property("label", label)
116 self.emit("appearance_changed")
117
119 """
120 Returns the current label of the bar.
121
122 @return: string.
123 """
124 return self.get_property("label")
125
127 __gproperties__ = {"draw-labels": (gobject.TYPE_BOOLEAN,
128 "draw bar labels",
129 "Set whether to draw bar labels.",
130 True, gobject.PARAM_READWRITE),
131 "show-values": (gobject.TYPE_BOOLEAN,
132 "show values",
133 "Set whether to show values in the bars' labels.",
134 True, gobject.PARAM_READWRITE),
135 "enable-mouseover": (gobject.TYPE_BOOLEAN,
136 "enable mouseover",
137 "Set whether a mouseover effect should be visible if moving the mouse over a bar.",
138 True, gobject.PARAM_READWRITE)}
139
140 __gsignals__ = {"bar-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))}
141
143 super(BarChart, self).__init__()
144 self._bars = []
145 self._enable_mouseover = True
146 self._values = True
147 self._labels = True
148 self._highlighted = None
149
150 self.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.SCROLL_MASK|gtk.gdk.POINTER_MOTION_MASK)
151 self.connect("button_press_event", self._cb_button_pressed)
152 self.connect("motion-notify-event", self._cb_motion_notify)
153
155 if property.name == "draw-labels":
156 return self._labels
157 elif property.name == "show-values":
158 return self._values
159 elif property.name == "enable-mouseover":
160 return self._enable_mouseover
161 else:
162 raise AttributeError, "Property %s does not exist." % property.name
163
165 if property.name == "draw-labels":
166 self._labels = value
167 elif property.name == "show-values":
168 self._values = value
169 elif property.name == "enable-mouseover":
170 self._enable_mouseover = value
171 else:
172 raise AttributeError, "Property %s does not exist." % property.name
173
176
178 if not self._enable_mouseover: return
179 bar = self._get_bar_at_pos(event.x, event.y)
180 if bar != self._highlighted:
181 self.queue_draw()
182 self._highlighted = bar
183
188
190 if not self._bars: return None
191 rect = self.get_allocation()
192
193 number_of_bars = len(self._bars)
194 max_value = max(x.get_value() for x in self._bars)
195 bar_padding = 16
196 bar_height_factor = .8
197 bar_vertical_padding = (1.0 - bar_height_factor) / 2.0
198 total_height = int(rect.height * bar_height_factor)
199 bottom = rect.height
200 bar_bottom = bottom * (1.0 - bar_vertical_padding)
201 bar_width = int((rect.width-(bar_padding*number_of_bars)) / number_of_bars)
202 for i,info in enumerate(self._bars):
203 bar_x = int(rect.width / float(number_of_bars) * i) + rect.x + (bar_padding // 2)
204 percent = float(info.get_value()) / float(max_value)
205 bar_height = int(total_height * percent)
206 bar_top = int(rect.height*bar_vertical_padding) + total_height - bar_height
207
208 if bar_x <= x <= bar_x+bar_width and bar_top <= y <= bar_bottom:
209 return info
210
211 return None
212
214 """
215 Draw the chart.
216
217 @type context: cairo.Context
218 @param context: The context to draw on.
219 @type rect: gtk.gdk.Rectangle
220 @param rect: A rectangle representing the charts area.
221 """
222 if not self._bars: return
223 number_of_bars = len(self._bars)
224 max_value = max(x.get_value() for x in self._bars)
225 bar_padding = 16
226 bar_height_factor = .8
227 bar_vertical_padding = (1.0 - bar_height_factor) / 2.0
228 total_height = int(rect.height * bar_height_factor)
229 bottom = rect.height
230 bar_bottom = bottom * (1.0 - bar_vertical_padding)
231 bar_width = int((rect.width-(bar_padding*number_of_bars)) / number_of_bars)
232
233 for i,info in enumerate(self._bars):
234 if not info.get_visible(): continue
235 x = int(rect.width / float(number_of_bars) * i) + rect.x + (bar_padding // 2)
236 percent = float(info.get_value()) / float(max_value)
237 bar_height = int(total_height * percent)
238 bar_top = int(rect.height*bar_vertical_padding) + total_height - bar_height
239
240
241 c = info.get_color()
242 context.set_source_rgb(c[0], c[1], c[2])
243 context.move_to(x, bar_bottom)
244 context.line_to(x, bar_top)
245 context.line_to(x+bar_width, bar_top)
246 context.line_to(x+bar_width, bar_bottom)
247 context.close_path()
248 context.fill()
249 context.stroke()
250
251 if info == self._highlighted:
252 context.set_source_rgba(1, 1, 1, 0.1)
253 context.move_to(x, bar_bottom)
254 context.line_to(x, bar_top)
255 context.line_to(x+bar_width, bar_top)
256 context.line_to(x+bar_width, bar_bottom)
257 context.close_path()
258 context.fill()
259 context.stroke()
260
261 if self._labels:
262
263 c = info.get_color()
264 context.set_source_rgb(c[0], c[1], c[2])
265 title = info.get_label()
266 label_height, label_width = context.text_extents(title)[3:5]
267 label_x = x + (bar_width // 2) - (label_width // 2)
268 context.move_to(label_x, bottom * .95)
269 context.show_text(title)
270 context.stroke()
271
272
273 count = '%d' % info.get_value()
274 count_height, count_width = context.text_extents(count)[3:5]
275 count_x = x + (bar_width // 2) - (count_width // 2)
276 context.move_to(count_x, bar_top-1)
277 context.show_text(count)
278 context.stroke()
279
280 - def draw(self, context):
281 """
282 Draw the widget. This method is called automatically. Don't call it
283 yourself. If you want to force a redrawing of the widget, call
284 the queue_draw() method.
285
286 @type context: cairo.Context
287 @param context: The context to draw on.
288 """
289 rect = self.get_allocation()
290
291 context.set_line_width(1)
292 font = gtk.Label().style.font_desc.get_family()
293 context.select_font_face(font,cairo.FONT_SLANT_NORMAL, \
294 cairo.FONT_WEIGHT_NORMAL)
295
296 self.draw_basics(context, rect)
297 self._do_draw_bars(context, rect)
298
304
306 """
307 Returns the Bar with the id 'name' if it exists, None
308 otherwise.
309
310 @type name: string
311 @param name: the id of a Bar
312
313 @return: Bar or None.
314 """
315 for bar in self._bars:
316 if bar.get_name() == name:
317 return bar
318 return None
319
321 """
322 Set whether to draw the labels of the bars.
323
324 @type draw: boolean.
325 """
326 self.set_property("draw-labels", draw)
327 self.queue_draw()
328
330 """
331 Returns True if bar labels are shown.
332
333 @return: boolean.
334 """
335 return self.get_property("draw-labels")
336
338 """
339 Set whether a mouseover effect should be shown when the pointer
340 enters a bar.
341
342 @type mouseover: boolean.
343 """
344 self.set_property("enable-mouseover", mouseover)
345
347 """
348 Returns True if the mouseover effect is enabled.
349
350 @return: boolean.
351 """
352 return self.get_property("enable-mouseover")
353
355 """
356 Set whether the bar's value should be shown in its label.
357
358 @type show: boolean.
359 """
360 self.set_property("show-values", show)
361 self.queue_draw()
362
364 """
365 Returns True if the value of a bar is shown in its label.
366
367 @return: boolean.
368 """
369 return self.get_property("show-values")
370
372 __gproperties__ = {"name": (gobject.TYPE_STRING, "bar name",
373 "A unique name for the bar.",
374 "", gobject.PARAM_READABLE),
375 "value": (gobject.TYPE_FLOAT,
376 "value",
377 "The value.",
378 0.0, 9999999999.0, 0.0, gobject.PARAM_READABLE),
379 "label": (gobject.TYPE_STRING, "bar label",
380 "The label for the bar.", "",
381 gobject.PARAM_READWRITE)}
382
384 super(MultiBar, self).__init__()
385 self._name = name
386 self._label = label
387 self.bars = []
388
390 if property.name == "visible":
391 return self._show
392 elif property.name == "antialias":
393 return self._antialias
394 elif property.name == "name":
395 return self._name
396 elif property.name == "value":
397 return max(x.get_value() for x in self.bars) if self.bars else 0.0
398 elif property.name == "label":
399 return self._label
400 else:
401 raise AttributeError, "Property %s does not exist." % property.name
402
404 if property.name == "visible":
405 self._show = value
406 elif property.name == "antialias":
407 self._antialias = value
408 elif property.name == "label":
409 self._label = value
410 else:
411 raise AttributeError, "Property %s does not exist." % property.name
412
414 """
415 Returns the maximum value of the MultiBar.
416
417 @return: float.
418 """
419 return self.get_property("value")
420
422 """
423 Set the label for the bar chart bar.
424
425 @param label: the new label
426 @type label: string.
427 """
428 self.set_property("label", label)
429 self.emit("appearance_changed")
430
432 """
433 Returns the current label of the bar.
434
435 @return: string.
436 """
437 return self.get_property("label")
438
444
446 """
447 Returns the Bar with the id 'name' if it exists, None
448 otherwise.
449
450 @type name: string
451 @param name: the id of a Bar
452
453 @return: Bar or None.
454 """
455 for bar in self.bars:
456 if bar.get_name() == name:
457 return bar
458 return None
459
461 self.emit("appearance_changed")
462
464 __gsignals__ = {"multibar-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,gobject.TYPE_PYOBJECT,))}
469
473
475 if not self._enable_mouseover: return
476 multibar, subbar = self._get_bar_at_pos(event.x, event.y)
477 if subbar != self._highlighted:
478 self.queue_draw()
479 self._highlighted = subbar
480
486
488 """
489 Draw the chart.
490
491 @type context: cairo.Context
492 @param context: The context to draw on.
493 @type rect: gtk.gdk.Rectangle
494 @param rect: A rectangle representing the charts area.
495 """
496 if not self._bars: return
497 number_of_bars = len(self._bars)
498 max_value = max(x.get_value() for x in self._bars)
499 bar_padding = 16
500 bar_height_factor = .8
501 bar_vertical_padding = (1.0 - bar_height_factor) / 2.0
502 total_height = int(rect.height * bar_height_factor)
503 bottom = rect.height
504 bar_bottom = bottom * (1.0 - bar_vertical_padding)
505 bar_width = int((rect.width-(bar_padding*number_of_bars)) / number_of_bars)
506
507 font_size = 12
508 context.set_font_size(font_size)
509
510 for bar_index, multibar in enumerate(self._bars):
511 if not multibar.get_visible(): continue
512 multibar_count = len(multibar.bars)
513 x = int(rect.width / float(number_of_bars) * bar_index) + rect.x + (bar_padding // 2)
514 max_rotated_height = 0
515 for sub_bar_index, sub_bar in enumerate(multibar.bars):
516 sub_bar_width = bar_width // multibar_count
517 sub_bar_x = x + sub_bar_width * sub_bar_index
518 percent = float(sub_bar.get_value()) / float(max_value)
519 bar_height = int(total_height * percent)
520 bar_top = int(rect.height*bar_vertical_padding) + total_height - bar_height
521
522
523 c = sub_bar.get_color()
524 context.set_source_rgb(c[0], c[1], c[2])
525 context.move_to(sub_bar_x, bar_bottom)
526 context.line_to(sub_bar_x, bar_top)
527 context.line_to(sub_bar_x+sub_bar_width, bar_top)
528 context.line_to(sub_bar_x+sub_bar_width, bar_bottom)
529 context.close_path()
530 context.fill()
531 context.stroke()
532
533 if sub_bar == self._highlighted:
534 context.set_source_rgba(1, 1, 1, 0.1)
535 context.move_to(sub_bar_x, bar_bottom)
536 context.line_to(sub_bar_x, bar_top)
537 context.line_to(sub_bar_x+sub_bar_width, bar_top)
538 context.line_to(sub_bar_x+sub_bar_width, bar_bottom)
539 context.close_path()
540 context.fill()
541 context.stroke()
542
543 if self._labels:
544
545 c = sub_bar.get_color()
546 context.set_source_rgb(c[0], c[1], c[2])
547 count = '%d' % sub_bar.get_value()
548 count_height, count_width = context.text_extents(count)[3:5]
549 count_x = sub_bar_x + (sub_bar_width // 2) - (count_width // 2)
550 context.move_to(count_x, bar_top-1)
551 context.show_text(count)
552 context.stroke()
553
554
555 title = sub_bar.get_label()
556 label_height, label_width = context.text_extents(title)[3:5]
557 rotation_rad = math.pi*self.sub_label_rotation_deg / 180.0
558 rotated_height = max(label_height, abs(math.sin(rotation_rad) * label_width))
559 rotated_width = max(label_height, abs(math.cos(rotation_rad) * label_width))
560 max_rotated_height = max(max_rotated_height, int(rotated_height)+1)
561 label_x = sub_bar_x + (sub_bar_width // 3)
562 context.move_to(label_x, bar_bottom + 10)
563 context.rotate(rotation_rad)
564 context.show_text(title)
565 context.rotate(-rotation_rad)
566 context.stroke()
567
568 if self._labels:
569
570 context.set_source_rgb(0, 0, 0)
571 title = multibar.get_label()
572 label_height, label_width = context.text_extents(title)[3:5]
573 label_x = x + (bar_width // 2) - (label_width // 2)
574 label_y = min(bottom, bar_bottom + max_rotated_height + 25)
575 context.move_to(label_x, label_y)
576 context.show_text(title)
577 context.stroke()
578
580 if not self._bars:
581 return None,None
582 rect = self.get_allocation()
583 number_of_bars = len(self._bars)
584 max_value = max(x.get_value() for x in self._bars)
585 bar_padding = 16
586 bar_height_factor = .8
587 bar_vertical_padding = (1.0 - bar_height_factor) / 2.0
588 total_height = int(rect.height * bar_height_factor)
589 bottom = rect.height
590 bar_bottom = bottom * (1.0 - bar_vertical_padding)
591 bar_width = int((rect.width-(bar_padding*number_of_bars)) / number_of_bars)
592 for bar_index, multibar in enumerate(self._bars):
593 if not multibar.get_visible(): continue
594 multibar_count = len(multibar.bars)
595 multibar_x = int(rect.width / float(number_of_bars) * bar_index) + rect.x + (bar_padding // 2)
596 max_rotated_height = 0
597 for sub_bar_index, sub_bar in enumerate(multibar.bars):
598 sub_bar_width = bar_width // multibar_count
599 sub_bar_x = multibar_x + sub_bar_width * sub_bar_index
600 percent = float(sub_bar.get_value()) / float(max_value)
601 bar_height = int(total_height * percent)
602 bar_top = int(rect.height*bar_vertical_padding) + total_height - bar_height
603
604 if sub_bar_x <= x <= sub_bar_x+sub_bar_width and bar_top <= y <= bar_bottom:
605 return multibar, sub_bar
606
607 return None,None
608