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

Source Code for Module pygtk_chart.bar_chart

  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 # for defaultdict 
 12  import math # for pi 
 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
43 - def do_get_property(self, property):
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
59 - def do_set_property(self, property, value):
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
73 - def set_value(self, value):
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
82 - def get_value(self):
83 """ 84 Returns the current value of the Bar. 85 86 @return: float. 87 """ 88 return self.get_property("value")
89
90 - def set_color(self, color):
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
100 - def get_color(self):
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
108 - def set_label(self, label):
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
118 - def get_label(self):
119 """ 120 Returns the current label of the bar. 121 122 @return: string. 123 """ 124 return self.get_property("label")
125
126 -class BarChart(chart.Chart):
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
142 - def __init__(self):
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
154 - def do_get_property(self, property):
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
164 - def do_set_property(self, property, value):
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
174 - def _cb_appearance_changed(self, widget):
175 self.queue_draw()
176
177 - def _cb_motion_notify(self, widget, event):
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
184 - def _cb_button_pressed(self, widget, event):
185 bar = self._get_bar_at_pos(event.x, event.y) 186 if bar: 187 self.emit("bar-clicked", bar)
188
189 - def _get_bar_at_pos(self, x, y):
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 # pixels of padding to either side of each bar 196 bar_height_factor = .8 # percentage of total height the bars will use 197 bar_vertical_padding = (1.0 - bar_height_factor) / 2.0 # space above and below the bars 198 total_height = int(rect.height * bar_height_factor) # maximum height for a bar 199 bottom = rect.height # y-value of bottom of bar chart 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
213 - def _do_draw_bars(self, context, rect):
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 # pixels of padding to either side of each bar 226 bar_height_factor = .8 # percentage of total height the bars will use 227 bar_vertical_padding = (1.0 - bar_height_factor) / 2.0 # space above and below the bars 228 total_height = int(rect.height * bar_height_factor) # maximum height for a bar 229 bottom = rect.height # y-value of bottom of bar chart 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 # draw the bar 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 # draw the label below the bar 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 # draw the count at the top of the bar 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 #initial context settings: line width & font 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
299 - def add_bar(self, bar):
300 color = bar.get_color() 301 if color == COLOR_AUTO: bar.set_color(COLORS[len(self._bars) % len(COLORS)]) 302 self._bars.append(bar) 303 bar.connect("appearance_changed", self._cb_appearance_changed)
304
305 - def get_bar(self, name):
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
320 - def set_draw_labels(self, draw):
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
329 - def get_draw_labels(self):
330 """ 331 Returns True if bar labels are shown. 332 333 @return: boolean. 334 """ 335 return self.get_property("draw-labels")
336
337 - def set_enable_mouseover(self, mouseover):
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
346 - def get_enable_mouseover(self):
347 """ 348 Returns True if the mouseover effect is enabled. 349 350 @return: boolean. 351 """ 352 return self.get_property("enable-mouseover")
353
354 - def set_show_values(self, show):
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
363 - def get_show_values(self):
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
371 -class MultiBar(chart.ChartObject):
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
383 - def __init__(self, name, label=""):
384 super(MultiBar, self).__init__() 385 self._name = name 386 self._label = label 387 self.bars = []
388
389 - def do_get_property(self, property):
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
403 - def do_set_property(self, property, value):
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
413 - def get_value(self):
414 """ 415 Returns the maximum value of the MultiBar. 416 417 @return: float. 418 """ 419 return self.get_property("value")
420
421 - def set_label(self, label):
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
431 - def get_label(self):
432 """ 433 Returns the current label of the bar. 434 435 @return: string. 436 """ 437 return self.get_property("label")
438
439 - def add_bar(self, bar):
440 color = bar.get_color() 441 if color == COLOR_AUTO: bar.set_color(COLORS[len(self.bars) % len(COLORS)]) 442 self.bars.append(bar) 443 bar.connect("appearance_changed", self._cb_appearance_changed)
444
445 - def get_bar(self, name):
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
460 - def _cb_appearance_changed(self, widget):
461 self.emit("appearance_changed")
462
463 -class MultiBarChart(BarChart):
464 __gsignals__ = {"multibar-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,gobject.TYPE_PYOBJECT,))}
465 - def __init__(self):
466 super(MultiBarChart, self).__init__() 467 self.name_map = {} 468 self.sub_label_rotation_deg = 20.0 # amout of rotation in the sub bar labels
469
470 - def add_bar(self, bar):
471 self._bars.append(bar) 472 bar.connect("appearance_changed", self._cb_appearance_changed)
473
474 - def _cb_motion_notify(self, widget, event):
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
481 - def _cb_button_pressed(self, widget, event):
482 multibar, subbar = self._get_bar_at_pos(event.x, event.y) 483 if subbar: 484 self.emit("multibar-clicked", multibar, subbar) 485 self.emit("bar-clicked", multibar)
486
487 - def _do_draw_bars(self, context, rect):
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 # pixels of padding to either side of each bar 500 bar_height_factor = .8 # percentage of total height the bars will use 501 bar_vertical_padding = (1.0 - bar_height_factor) / 2.0 # space above and below the bars 502 total_height = int(rect.height * bar_height_factor) # maximum height for a bar 503 bottom = rect.height # y-value of bottom of bar chart 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 # draw the bar 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 # draw the count at the top of the bar 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 # draw the label below the bar 554 #context.set_source_rgb(0, 0, 0) 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 # draw the label below the bar 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
579 - def _get_bar_at_pos(self, x, y):
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 # pixels of padding to either side of each bar 586 bar_height_factor = .8 # percentage of total height the bars will use 587 bar_vertical_padding = (1.0 - bar_height_factor) / 2.0 # space above and below the bars 588 total_height = int(rect.height * bar_height_factor) # maximum height for a bar 589 bottom = rect.height # y-value of bottom of bar chart 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