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

Source Code for Module pygtk_chart.line_chart

   1  #!/usr/bin/env python 
   2  # 
   3  #       lineplot.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 LineChart widget. 
  23   
  24  Author: Sven Festersen (sven@sven-festersen.de) 
  25  """ 
  26  __docformat__ = "epytext" 
  27  import gobject 
  28  import cairo 
  29  import gtk 
  30  import math 
  31   
  32  import os 
  33   
  34  from pygtk_chart.basics import * 
  35  from pygtk_chart import chart 
  36   
  37  RANGE_AUTO = 0 
  38  GRAPH_PADDING = 1 / 15.0 #a relative padding 
  39  GRAPH_POINTS = 1 
  40  GRAPH_LINES = 2 
  41  GRAPH_BOTH = 3 
  42  COLOR_AUTO = 4 
  43  POSITION_AUTO = 5 
  44  POSITION_LEFT = 6 
  45  POSITION_RIGHT = 7 
  46  POSITION_BOTTOM = 6 
  47  POSITION_TOP = 7 
  48   
  49  #load default color palette 
  50  COLORS = color_list_from_file(os.path.dirname(__file__) + "/data/tango.color") 
  51   
  52   
53 -class RangeCalculator:
54 """ 55 This helper class calculates ranges. It is used by the LineChart 56 widget internally, there is no need to create an instance yourself. 57 """
58 - def __init__(self):
59 self._data_xrange = None 60 self._data_yrange = None 61 self._xrange = RANGE_AUTO 62 self._yrange = RANGE_AUTO 63 self._cached_xtics = [] 64 self._cached_ytics = []
65
66 - def add_graph(self, graph):
67 if self._data_xrange == None: 68 self._data_yrange = graph.get_y_range() 69 self._data_xrange = graph.get_x_range() 70 else: 71 yrange = graph.get_y_range() 72 xrange = graph.get_x_range() 73 74 if xrange and yrange: 75 xmin = min(xrange[0], self._data_xrange[0]) 76 xmax = max(xrange[1], self._data_xrange[1]) 77 ymin = min(yrange[0], self._data_yrange[0]) 78 ymax = max(yrange[1], self._data_yrange[1]) 79 80 self._data_xrange = (xmin, xmax) 81 self._data_yrange = (ymin, ymax)
82
83 - def get_ranges(self):
84 xrange = self._xrange 85 if xrange == RANGE_AUTO: 86 xrange = self._data_xrange 87 if xrange[0] == xrange[1]: 88 xrange = (xrange[0], xrange[0] + 0.1) 89 90 yrange = self._yrange 91 if yrange == RANGE_AUTO: 92 yrange = self._data_yrange 93 if yrange[0] == yrange[1]: 94 yrange = (yrange[0], yrange[0] + 0.1) 95 96 return (xrange, yrange)
97
98 - def set_xrange(self, xrange):
99 self._xrange = xrange
100
101 - def set_yrange(self, yrange):
102 self._yrange = yrange
103
104 - def get_absolute_zero(self, rect):
105 xrange, yrange = self.get_ranges() 106 107 xfactor = float(rect.width * (1 - 2 * GRAPH_PADDING)) / (xrange[1] - xrange[0]) 108 yfactor = float(rect.height * (1 - 2 * GRAPH_PADDING)) / (yrange[1] - yrange[0]) 109 zx = (rect.width * GRAPH_PADDING) - xrange[0] * xfactor 110 zy = rect.height - ((rect.height * GRAPH_PADDING) - yrange[0] * yfactor) 111 112 return (zx,zy)
113
114 - def get_absolute_point(self, rect, x, y):
115 (zx, zy) = self.get_absolute_zero(rect) 116 xrange, yrange = self.get_ranges() 117 118 xfactor = float(rect.width * (1 - 2 * GRAPH_PADDING)) / (xrange[1] - xrange[0]) 119 yfactor = float(rect.height * (1 - 2 * GRAPH_PADDING)) / (yrange[1] - yrange[0]) 120 121 ax = zx + x * xfactor 122 ay = zy - y * yfactor 123 return (ax, ay)
124
125 - def prepare_tics(self, rect):
126 self._cached_xtics = self._get_xtics(rect) 127 self._cached_ytics = self._get_ytics(rect)
128
129 - def get_xtics(self, rect):
130 return self._cached_xtics
131
132 - def get_ytics(self, rect):
133 return self._cached_ytics
134
135 - def _get_xtics(self, rect):
136 tics = [] 137 (zx, zy) = self.get_absolute_zero(rect) 138 (xrange, yrange) = self.get_ranges() 139 delta = xrange[1] - xrange[0] 140 exp = int(math.log10(delta)) - 1 141 142 first_n = int(xrange[0] / (10 ** exp)) 143 last_n = int(xrange[1] / (10 ** exp)) 144 n = last_n - first_n 145 N = rect.width / 50.0 146 divide_by = int(n / N) 147 if divide_by == 0: divide_by = 1 148 149 left = rect.width * GRAPH_PADDING 150 right = rect.width * (1 - GRAPH_PADDING) 151 152 for i in range(first_n, last_n + 1): 153 num = i * 10 ** exp 154 (x, y) = self.get_absolute_point(rect, num, 0) 155 if i % divide_by == 0 and is_in_range(x, (left, right)): 156 tics.append(((x, y), num)) 157 158 return tics
159
160 - def _get_ytics(self, rect):
161 tics = [] 162 (zx, zy) = self.get_absolute_zero(rect) 163 (xrange, yrange) = self.get_ranges() 164 delta = yrange[1] - yrange[0] 165 exp = int(math.log10(delta)) - 1 166 167 first_n = int(yrange[0] / (10 ** exp)) 168 last_n = int(yrange[1] / (10 ** exp)) 169 n = last_n - first_n 170 N = rect.height / 50.0 171 divide_by = int(n / N) 172 if divide_by == 0: divide_by = 1 173 174 top = rect.height * GRAPH_PADDING 175 bottom = rect.height * (1 - GRAPH_PADDING) 176 177 for i in range(first_n, last_n + 1): 178 num = i * 10 ** exp 179 (x, y) = self.get_absolute_point(rect, 0, num) 180 if i % divide_by == 0 and is_in_range(y, (top, bottom)): 181 tics.append(((x, y), num)) 182 183 return tics
184 185
186 -class LineChart(chart.Chart):
187 """ 188 A widget that shows a line chart. The following objects can be accessed: 189 - LineChart.background (inherited from chart.Chart) 190 - LineChart.title (inherited from chart.Chart) 191 - LineChart.graphs 192 - LineChart.grid 193 - LineChart.xaxis 194 - LineChart.yaxis 195 """
196 - def __init__(self):
197 chart.Chart.__init__(self) 198 self.graphs = {} 199 self._range_calc = RangeCalculator() 200 self.xaxis = XAxis(self._range_calc) 201 self.yaxis = YAxis(self._range_calc) 202 self.grid = Grid(self._range_calc) 203 204 self.xaxis.connect("appearance_changed", self._cb_appearance_changed) 205 self.yaxis.connect("appearance_changed", self._cb_appearance_changed) 206 self.grid.connect("appearance_changed", self._cb_appearance_changed)
207
208 - def _do_draw_graphs(self, context, rect):
209 """ 210 Draw all the graphs. 211 212 @type context: cairo.Context 213 @param context: The context to draw on. 214 @type rect: gtk.gdk.Rectangle 215 @param rect: A rectangle representing the charts area. 216 """ 217 for (name, graph) in self.graphs.iteritems(): 218 graph.draw(context, rect)
219
220 - def _do_draw_axes(self, context, rect):
221 """ 222 Draw x and y axis. 223 224 @type context: cairo.Context 225 @param context: The context to draw on. 226 @type rect: gtk.gdk.Rectangle 227 @param rect: A rectangle representing the charts area. 228 """ 229 self.xaxis.draw(context, rect, self.yaxis) 230 self.yaxis.draw(context, rect, self.xaxis)
231
232 - def draw(self, context):
233 """ 234 Draw the widget. This method is called automatically. Don't call it 235 yourself. If you want to force a redrawing of the widget, call 236 the queue_draw() method. 237 238 @type context: cairo.Context 239 @param context: The context to draw on. 240 """ 241 rect = self.get_allocation() 242 self._range_calc.prepare_tics(rect) 243 #initial context settings: line width & font 244 context.set_line_width(1) 245 font = gtk.Label().style.font_desc.get_family() 246 context.select_font_face(font,cairo.FONT_SLANT_NORMAL, \ 247 cairo.FONT_WEIGHT_NORMAL) 248 249 self.draw_basics(context, rect) 250 data_available = False 251 for (name, graph) in self.graphs.iteritems(): 252 if graph.has_something_to_draw(): 253 data_available = True 254 break 255 256 if self.graphs and data_available: 257 self.grid.draw(context, rect) 258 self._do_draw_graphs(context, rect) 259 self._do_draw_axes(context, rect)
260
261 - def add_graph(self, graph):
262 """ 263 Add a graph object to the plot. 264 265 @type graph: line_chart.Graph 266 @param graph: The graph to add. 267 """ 268 if graph.get_color() == COLOR_AUTO: 269 graph.set_color(COLORS[len(self.graphs) % len(COLORS)]) 270 graph.set_range_calc(self._range_calc) 271 self.graphs[graph.get_name()] = graph 272 self._range_calc.add_graph(graph) 273 274 graph.connect("appearance-changed", self._cb_appearance_changed)
275
276 - def remove_graph(self, name):
277 """ 278 Remove a graph from the plot. 279 280 @type name: string 281 @param name: The name of the graph to remove. 282 """ 283 del self.graphs[name] 284 self.queue_draw()
285
286 - def set_xrange(self, xrange):
287 """ 288 Set the visible xrange. xrange has to be a pair: (xmin, xmax) or 289 RANGE_AUTO. If you set it to RANGE_AUTO, the visible range will 290 be calculated. 291 292 @type xrange: pair of numbers 293 @param xrange: The new xrange. 294 """ 295 self._range_calc.set_xrange(xrange)
296
297 - def set_yrange(self, yrange):
298 """ 299 Set the visible yrange. yrange has to be a pair: (ymin, ymax) or 300 RANGE_AUTO. If you set it to RANGE_AUTO, the visible range will 301 be calculated. 302 303 @type yrange: pair of numbers 304 @param yrange: The new yrange. 305 """ 306 self._range_calc.set_yrange(yrange)
307 308
309 -class Axis(chart.ChartObject):
310 311 __gproperties__ = {"label": (gobject.TYPE_STRING, "axis label", 312 "The label of the axis.", "", 313 gobject.PARAM_READWRITE), 314 "show-label": (gobject.TYPE_BOOLEAN, "show label", 315 "Set whether to show the axis label.", 316 True, gobject.PARAM_READWRITE), 317 "position": (gobject.TYPE_INT, "axis position", 318 "Position of the axis.", 5, 7, 5, 319 gobject.PARAM_READWRITE), 320 "show-tics": (gobject.TYPE_BOOLEAN, "show tics", 321 "Set whether to draw tics.", True, 322 gobject.PARAM_READWRITE), 323 "show-tic-labels": (gobject.TYPE_BOOLEAN, 324 "show tic labels", 325 "Set whether to draw tic labels", 326 True, 327 gobject.PARAM_READWRITE), 328 "tic-format-function": (gobject.TYPE_PYOBJECT, 329 "tic format function", 330 "This function is used to label the tics.", 331 gobject.PARAM_READWRITE)} 332
333 - def __init__(self, range_calc, label):
334 chart.ChartObject.__init__(self) 335 self.set_property("antialias", False) 336 337 self._label = label 338 self._show_label = True 339 self._position = POSITION_AUTO 340 self._show_tics = True 341 self._show_tic_labels = True 342 self._tic_format_function = str 343 344 self._range_calc = range_calc
345
346 - def do_get_property(self, property):
347 if property.name == "visible": 348 return self._show 349 elif property.name == "antialias": 350 return self._antialias 351 elif property.name == "label": 352 return self._label 353 elif property.name == "show-label": 354 return self._show_label 355 elif property.name == "position": 356 return self._position 357 elif property.name == "show-tics": 358 return self._show_tics 359 elif property.name == "show-tic-labels": 360 return self._show_tic_labels 361 elif property.name == "tic-format-function": 362 return self._tic_format_function 363 else: 364 raise AttributeError, "Property %s does not exist." % property.name
365
366 - def do_set_property(self, property, value):
367 if property.name == "visible": 368 self._show = value 369 elif property.name == "antialias": 370 self._antialias = value 371 elif property.name == "label": 372 self._label = value 373 elif property.name == "show-label": 374 self._show_label = value 375 elif property.name == "position": 376 self._position = value 377 elif property.name == "show-tics": 378 self._show_tics = value 379 elif property.name == "show-tic-labels": 380 self._show_tic_labels = value 381 elif property.name == "tic-format-function": 382 self._tic_format_function = value 383 else: 384 raise AttributeError, "Property %s does not exist." % property.name
385
386 - def set_label(self, label):
387 """ 388 Set the label of the axis. 389 390 @param label: new label 391 @type label: string. 392 """ 393 self.set_property("label", label) 394 self.emit("appearance_changed")
395
396 - def get_label(self):
397 """ 398 Returns the current label of the axis. 399 400 @return: string. 401 """ 402 return self.get_property("label")
403
404 - def set_show_label(self, show):
405 """ 406 Set whether to show the axis' label. 407 408 @type show: boolean. 409 """ 410 self.set_property("show-label", show) 411 self.emit("appearance_changed")
412
413 - def get_show_label(self):
414 """ 415 Returns True if the axis' label is shown. 416 417 @return: boolean. 418 """ 419 return self.get_property("show-label")
420
421 - def set_position(self, pos):
422 """ 423 Set the position of the axis. pos hast to be one these 424 constants: POSITION_AUTO, POSITION_BOTTOM, POSITION_LEFT, 425 POSITION_RIGHT, POSITION_TOP. 426 """ 427 self.set_property("position", pos) 428 self.emit("appearance_changed")
429
430 - def get_position(self):
431 """ 432 Returns the position of the axis. (see set_position for 433 details). 434 """ 435 return self.get_property("position")
436
437 - def set_show_tics(self, show):
438 """ 439 Set whether to draw tics at the axis. 440 441 @type show: boolean. 442 """ 443 self.set_property("show-tics", show) 444 self.emit("appearance_changed")
445
446 - def get_show_tics(self):
447 """ 448 Returns True if tics are drawn. 449 450 @return: boolean. 451 """ 452 return self.get_property("show-tics")
453
454 - def set_show_tic_labels(self, show):
455 """ 456 Set whether to draw tic labels. Labels are only drawn if 457 tics are drawn. 458 459 @type show: boolean. 460 """ 461 self.set_property("show-tic-labels", show) 462 self.emit("appearance_changed")
463
464 - def get_show_tic_labels(self):
465 """ 466 Returns True if tic labels are shown. 467 468 @return: boolean. 469 """ 470 return self.get_property("show-tic-labels")
471
472 - def set_tic_format_function(self, func):
473 """ 474 Use this to set the function that should be used to label 475 the tics. The function should take a number as the only 476 argument and return a string. Default: str 477 478 @type func: function. 479 """ 480 self.set_property("tic-format-function", func) 481 self.emit("appearance_changed")
482
483 - def get_tic_format_function(self):
484 """ 485 Returns the function currently used for labeling the tics. 486 """ 487 return self.get_property("tic-format-function")
488 489
490 -class XAxis(Axis):
491 """ 492 This class represents the xaxis. It is used by the LineChart 493 widget internally, there is no need to create an instance yourself. 494 """
495 - def __init__(self, range_calc):
496 Axis.__init__(self, range_calc, "x")
497
498 - def draw(self, context, rect, yaxis):
499 """ 500 This method is called by the parent Plot instance. It 501 calls _do_draw. 502 """ 503 if self._show: 504 if not self._antialias: 505 context.set_antialias(cairo.ANTIALIAS_NONE) 506 self._do_draw(context, rect, yaxis) 507 context.set_antialias(cairo.ANTIALIAS_DEFAULT)
508
509 - def _do_draw_tics(self, context, rect, yaxis):
510 if self._show_tics: 511 tics = self._range_calc.get_xtics(rect) 512 513 #select font size 514 font_size = rect.height / 50 515 if font_size < 9: font_size = 9 516 context.set_font_size(font_size) 517 518 for ((x,y), label) in tics: 519 if self._position == POSITION_TOP: 520 y = rect.height * GRAPH_PADDING 521 elif self._position == POSITION_BOTTOM: 522 y = rect.height * (1 - GRAPH_PADDING) 523 tic_height = rect.height / 80.0 524 context.move_to(x, y + tic_height / 2) 525 context.rel_line_to(0, - tic_height) 526 context.stroke() 527 528 if self._show_tic_labels: 529 if label == 0 and self._position == POSITION_AUTO and yaxis.get_position() == POSITION_AUTO: 530 label = " " 531 else: 532 label = self._tic_format_function(label) 533 size = context.text_extents(label) 534 x = x - size[2] / 2 535 y = y + size[3] + font_size / 2 536 if self._position == POSITION_TOP: 537 y = y - size[3] - font_size / 2 - tic_height 538 if label[0] == "-": 539 x = x - context.text_extents("-")[2] 540 context.move_to(x, y) 541 context.show_text(label) 542 context.stroke()
543
544 - def _do_draw_label(self, context, rect, pos):
545 (x, y) = pos 546 font_size = rect.height / 50 547 if font_size < 9: font_size = 9 548 context.set_font_size(font_size) 549 size = context.text_extents(self._label) 550 x = x + size[2] / 2 551 y = y + size[3] 552 553 context.move_to(x, y) 554 context.show_text(self._label) 555 context.stroke()
556
557 - def _do_draw(self, context, rect, yaxis):
558 """ 559 Draw the axis. 560 """ 561 (zx, zy) = self._range_calc.get_absolute_zero(rect) 562 if self._position == POSITION_BOTTOM: 563 zy = rect.height * (1 - GRAPH_PADDING) 564 elif self._position == POSITION_TOP: 565 zy = rect.height * GRAPH_PADDING 566 if rect.height * GRAPH_PADDING <= zy and rect.height * (1 - GRAPH_PADDING) >= zy: 567 context.set_source_rgb(0, 0, 0) 568 #draw the line: 569 context.move_to(rect.width * GRAPH_PADDING, zy) 570 context.line_to(rect.width * (1 - GRAPH_PADDING), zy) 571 context.stroke() 572 #draw arrow: 573 context.move_to(rect.width * (1 - GRAPH_PADDING) + 3, zy) 574 context.rel_line_to(-3, -3) 575 context.rel_line_to(0, 6) 576 context.close_path() 577 context.fill() 578 579 if self._show_label: 580 self._do_draw_label(context, rect, (rect.width * (1 - GRAPH_PADDING) + 3, zy)) 581 self._do_draw_tics(context, rect, yaxis)
582 583
584 -class YAxis(Axis):
585 """ 586 This class represents the yaxis. It is used by the LineChart 587 widget internally, there is no need to create an instance yourself. 588 """
589 - def __init__(self, range_calc):
590 Axis.__init__(self, range_calc, "y")
591
592 - def draw(self, context, rect, xaxis):
593 """ 594 This method is called by the parent Plot instance. It 595 calls _do_draw. 596 """ 597 if self._show: 598 if not self._antialias: 599 context.set_antialias(cairo.ANTIALIAS_NONE) 600 self._do_draw(context, rect, xaxis) 601 context.set_antialias(cairo.ANTIALIAS_DEFAULT)
602
603 - def _do_draw_tics(self, context, rect, xaxis):
604 if self._show_tics: 605 tics = self._range_calc.get_ytics(rect) 606 607 #select font size 608 font_size = rect.height / 50 609 #if font_size < 9: font_size = 9 610 context.set_font_size(font_size) 611 612 for ((x,y), label) in tics: 613 if self._position == POSITION_LEFT: 614 x = rect.width * GRAPH_PADDING 615 elif self._position == POSITION_RIGHT: 616 x = rect.width * (1 - GRAPH_PADDING) 617 tic_width = rect.height / 80.0 618 context.move_to(x + tic_width / 2, y) 619 context.rel_line_to(- tic_width, 0) 620 context.stroke() 621 622 if self._show_tic_labels: 623 if label == 0 and self._position == POSITION_AUTO and xaxis.get_position() == POSITION_AUTO: 624 label = " " 625 else: 626 label = self._tic_format_function(label) 627 size = context.text_extents(label) 628 x = x - size[2] - font_size / 2 629 if self._position == POSITION_RIGHT: 630 x = x + size[2] + font_size / 2 + tic_width 631 y = y + size[3] / 2 632 if label[0] == "-": 633 x = x - context.text_extents("-")[2] 634 context.move_to(x, y) 635 context.show_text(label) 636 context.stroke()
637
638 - def _do_draw_label(self, context, rect, pos):
639 (x, y) = pos 640 font_size = rect.height / 50 641 if font_size < 9: font_size = 9 642 context.set_font_size(font_size) 643 size = context.text_extents(self._label) 644 x = x - size[2] 645 y = y - size[3] / 2 646 647 context.move_to(x, y) 648 context.show_text(self._label) 649 context.stroke()
650
651 - def _do_draw(self, context, rect, xaxis):
652 (zx, zy) = self._range_calc.get_absolute_zero(rect) 653 if self._position == POSITION_LEFT: 654 zx = rect.width * GRAPH_PADDING 655 elif self._position == POSITION_RIGHT: 656 zx = rect.width * (1 - GRAPH_PADDING) 657 if rect.width * GRAPH_PADDING <= zx and rect.width * (1 - GRAPH_PADDING) >= zx: 658 context.set_source_rgb(0, 0, 0) 659 #draw line: 660 context.move_to(zx, rect.height * (1 - GRAPH_PADDING)) 661 context.line_to(zx, rect.height * GRAPH_PADDING) 662 context.stroke() 663 #draw arrow: 664 context.move_to(zx, rect.height * GRAPH_PADDING - 3) 665 context.rel_line_to(-3, 3) 666 context.rel_line_to(6, 0) 667 context.close_path() 668 context.fill() 669 670 if self._show_label: 671 self._do_draw_label(context, rect, (zx, rect.height * GRAPH_PADDING - 3)) 672 self._do_draw_tics(context, rect, xaxis)
673 674
675 -class Grid(chart.ChartObject):
676 """ 677 A class representing the grid of the chart. It is used by the LineChart 678 widget internally, there is no need to create an instance yourself. 679 """ 680 681 __gproperties__ = {"show-horizontal": (gobject.TYPE_BOOLEAN, 682 "show horizontal lines", 683 "Set whether to draw horizontal lines.", 684 True, gobject.PARAM_READWRITE), 685 "show-vertical": (gobject.TYPE_BOOLEAN, 686 "show vertical lines", 687 "Set whether to draw vertical lines.", 688 True, gobject.PARAM_READWRITE), 689 "color": (gobject.TYPE_PYOBJECT, 690 "grid color", 691 "The color of the grid in (r,g,b) format. r,g,b in [0,1]", 692 gobject.PARAM_READWRITE)} 693
694 - def __init__(self, range_calc):
695 chart.ChartObject.__init__(self) 696 self.set_property("antialias", False) 697 self._range_calc = range_calc 698 self._color = (0.9, 0.9, 0.9) 699 self._show_h = True 700 self._show_v = True
701
702 - def do_get_property(self, property):
703 if property.name == "visible": 704 return self._show 705 elif property.name == "antialias": 706 return self._antialias 707 elif property.name == "show-horizontal": 708 return self._show_h 709 elif property.name == "show-vertical": 710 return self._show_v 711 elif property.name == "color": 712 return self._color 713 else: 714 raise AttributeError, "Property %s does not exist." % property.name
715
716 - def do_set_property(self, property, value):
717 if property.name == "visible": 718 self._show = value 719 elif property.name == "antialias": 720 self._antialias = value 721 elif property.name == "show-horizontal": 722 self._show_h = value 723 elif property.name == "show-vertical": 724 self._show_v = value 725 elif property.name == "color": 726 self._color = value 727 else: 728 raise AttributeError, "Property %s does not exist." % property.name
729
730 - def _do_draw(self, context, rect):
731 c = self._color 732 context.set_source_rgb(c[0], c[1], c[2]) 733 #draw horizontal lines 734 if self._show_h: 735 ytics = self._range_calc.get_ytics(rect) 736 xa = rect.width * GRAPH_PADDING 737 xb = rect.width * (1 - GRAPH_PADDING) 738 for ((x, y), label) in ytics: 739 context.move_to(xa, y) 740 context.line_to(xb, y) 741 context.stroke() 742 743 #draw vertical lines 744 if self._show_v: 745 xtics = self._range_calc.get_xtics(rect) 746 ya = rect.height * GRAPH_PADDING 747 yb = rect.height * (1 - GRAPH_PADDING) 748 for ((x, y), label) in xtics: 749 context.move_to(x, ya) 750 context.line_to(x, yb) 751 context.stroke()
752
753 - def set_draw_horizontal_lines(self, draw):
754 """ 755 Set whether to draw horizontal grid lines. 756 757 @type draw: boolean. 758 """ 759 self.set_property("show-horizontal", draw) 760 self.emit("appearance_changed")
761
762 - def get_draw_horizontal_lines(self):
763 """ 764 Returns True if horizontal grid lines are drawn. 765 766 @return: boolean. 767 """ 768 return self.get_property("show-horizontal")
769
770 - def set_draw_vertical_lines(self, draw):
771 """ 772 Set whether to draw vertical grid lines. 773 774 @type draw: boolean. 775 """ 776 self.set_property("show-vertical", draw) 777 self.emit("appearance_changed")
778
779 - def get_draw_vertical_lines(self):
780 """ 781 Returns True if vertical grid lines are drawn. 782 783 @return: boolean. 784 """ 785 return self.get_property("show-vertical")
786
787 - def set_color(self, color):
788 """ 789 Set the color of the grid. 790 791 @type color: a color 792 @param color: The new color of the grid. 793 """ 794 self.set_property("color", color) 795 self.emit("appearance_changed")
796
797 - def get_color(self):
798 """ 799 Returns the color of the grid. 800 801 @return: a color. 802 """ 803 return self.get_property("color")
804 805
806 -class Graph(chart.ChartObject):
807 """ 808 This class represents a graph or the data you want to plot on your 809 LineChart widget. 810 """ 811 812 __gproperties__ = {"name": (gobject.TYPE_STRING, "graph id", 813 "The graph's unique name.", 814 "", gobject.PARAM_READABLE), 815 "title": (gobject.TYPE_STRING, "graph title", 816 "The title of the graph.", "", 817 gobject.PARAM_READWRITE), 818 "color": (gobject.TYPE_PYOBJECT, 819 "graph color", 820 "The color of the graph in (r,g,b) format. r,g,b in [0,1].", 821 gobject.PARAM_READWRITE), 822 "type": (gobject.TYPE_INT, "graph type", 823 "The type of the graph.", 1, 3, 3, 824 gobject.PARAM_READWRITE), 825 "point-size": (gobject.TYPE_INT, "point size", 826 "Radius of the data points.", 1, 827 100, 2, gobject.PARAM_READWRITE), 828 "fill-to": (gobject.TYPE_PYOBJECT, "fill to", 829 "Set how to fill space under the graph.", 830 gobject.PARAM_READWRITE), 831 "fill-color": (gobject.TYPE_PYOBJECT, "fill color", 832 "Set which color to use when filling space under the graph.", 833 gobject.PARAM_READWRITE), 834 "fill-opacity": (gobject.TYPE_FLOAT, "fill opacity", 835 "Set which opacity to use when filling space under the graph.", 836 0.0, 1.0, 0.3, gobject.PARAM_READWRITE), 837 "show-values": (gobject.TYPE_BOOLEAN, "show values", 838 "Sets whether to show the y values.", 839 False, gobject.PARAM_READWRITE), 840 "show-title": (gobject.TYPE_BOOLEAN, "show title", 841 "Sets whether to show the graph's title.", 842 True, gobject.PARAM_READWRITE)} 843
844 - def __init__(self, name, title, data):
845 """ 846 Create a new instance. 847 848 @type name: string 849 @param name: A unique name for the graph. This could be everything. 850 It's just a name used internally for identification. You need to know 851 this if you want to access or delete a graph from a chart. 852 @type title: string 853 @param title: The graphs title. This can be drawn on the chart. 854 @type data: list of pairs of numbers 855 @param data: This is the data you want to be visualized. data has to 856 be a list of (x, y) pairs. 857 """ 858 chart.ChartObject.__init__(self) 859 self._name = name 860 self._title = title 861 self._data = data 862 self._color = COLOR_AUTO 863 self._type = GRAPH_BOTH 864 self._point_size = 2 865 self._show_value = False 866 self._show_title = True 867 self._fill_to = None 868 self._fill_color = COLOR_AUTO 869 self._fill_opacity = 0.3 870 871 self._range_calc = None
872
873 - def do_get_property(self, property):
874 if property.name == "visible": 875 return self._show 876 elif property.name == "antialias": 877 return self._antialias 878 elif property.name == "name": 879 return self._name 880 elif property.name == "title": 881 return self._title 882 elif property.name == "color": 883 return self._color 884 elif property.name == "type": 885 return self._type 886 elif property.name == "point-size": 887 return self._point_size 888 elif property.name == "fill-to": 889 return self._fill_to 890 elif property.name == "fill-color": 891 return self._fill_color 892 elif property.name == "fill-opacity": 893 return self._fill_opacity 894 elif property.name == "show-values": 895 return self._show_value 896 elif property.name == "show-title": 897 return self._show_title 898 else: 899 raise AttributeError, "Property %s does not exist." % property.name
900
901 - def do_set_property(self, property, value):
902 if property.name == "visible": 903 self._show = value 904 elif property.name == "antialias": 905 self._antialias = value 906 elif property.name == "title": 907 self._title = value 908 elif property.name == "color": 909 self._color = value 910 elif property.name == "type": 911 self._type = value 912 elif property.name == "point-size": 913 self._point_size = value 914 elif property.name == "fill-to": 915 self._fill_to = value 916 elif property.name == "fill-color": 917 self._fill_color = value 918 elif property.name == "fill-opacity": 919 self._fill_opacity = value 920 elif property.name == "show-values": 921 self._show_value = value 922 elif property.name == "show-title": 923 self._show_title = value 924 else: 925 raise AttributeError, "Property %s does not exist." % property.name
926
927 - def has_something_to_draw(self):
928 return self._data != []
929
930 - def _do_draw_title(self, context, rect, last_point):
931 """ 932 Draws the title. 933 934 @type context: cairo.Context 935 @param context: The context to draw on. 936 @type rect: gtk.gdk.Rectangle 937 @param rect: A rectangle representing the charts area. 938 @type last_point: pairs of numbers 939 @param last_point: The absolute position of the last drawn data point. 940 """ 941 c = self._color 942 context.set_source_rgb(c[0], c[1], c[2]) 943 944 font_size = rect.height / 50 945 if font_size < 9: font_size = 9 946 context.set_font_size(font_size) 947 size = context.text_extents(self._title) 948 if last_point: 949 context.move_to(last_point[0] + 5, last_point[1] + size[3] / 3) 950 context.show_text(self._title) 951 context.stroke()
952
953 - def _do_draw_fill(self, context, rect, xrange):
954 if type(self._fill_to) in (int, float): 955 data = [] 956 for i, (x, y) in enumerate(self._data): 957 if is_in_range(x, xrange) and not data: 958 data.append((x, self._fill_to)) 959 elif not is_in_range(x, xrange) and len(data) == 1: 960 data.append((prev, self._fill_to)) 961 break 962 elif i == len(self._data) - 1: 963 data.append((x, self._fill_to)) 964 prev = x 965 graph = Graph("none", "", data) 966 elif type(self._fill_to) == Graph: 967 graph = self._fill_to 968 d = graph.get_data() 969 range_b = d[0][0], d[-1][0] 970 xrange = intersect_ranges(xrange, range_b) 971 972 if not graph.get_visible(): return 973 974 c = self._fill_color 975 if c == COLOR_AUTO: c = self._color 976 context.set_source_rgba(c[0], c[1], c[2], self._fill_opacity) 977 978 data_a = self._data 979 data_b = graph.get_data() 980 981 first = True 982 start_point = (0, 0) 983 for x, y in data_a: 984 if is_in_range(x, xrange): 985 xa, ya = self._range_calc.get_absolute_point(rect, x, y) 986 if first: 987 context.move_to(xa, ya) 988 start_point = xa, ya 989 first = False 990 else: 991 context.line_to(xa, ya) 992 993 first = True 994 for i in range(0, len(data_b)): 995 j = len(data_b) - i - 1 996 x, y = data_b[j] 997 xa, ya = self._range_calc.get_absolute_point(rect, x, y) 998 if is_in_range(x, xrange): 999 context.line_to(xa, ya) 1000 context.line_to(*start_point) 1001 context.fill()
1002
1003 - def _do_draw(self, context, rect):
1004 """ 1005 Draw the graph. 1006 1007 @type context: cairo.Context 1008 @param context: The context to draw on. 1009 @type rect: gtk.gdk.Rectangle 1010 @param rect: A rectangle representing the charts area. 1011 """ 1012 #self._data.sort(lambda x, y: cmp(x[0], y[0])) 1013 (xrange, yrange) = self._range_calc.get_ranges() 1014 c = self._color 1015 context.set_source_rgb(c[0], c[1], c[2]) 1016 previous = None #previous is set to None if a point is not in range 1017 last = None #last will not be set to None in this case 1018 for (x, y) in self._data: 1019 if is_in_range(x, xrange) and is_in_range(y, yrange): 1020 (ax, ay) = self._range_calc.get_absolute_point(rect, x, y) 1021 if self._type == GRAPH_POINTS or self._type == GRAPH_BOTH: 1022 context.arc(ax, ay, self._point_size, 0, 2 * math.pi) 1023 context.fill() 1024 context.move_to(ax+2*self._point_size,ay) 1025 if self._show_value: 1026 font_size = rect.height / 50 1027 if font_size < 9: font_size = 9 1028 context.set_font_size(font_size) 1029 context.show_text(str(y)) 1030 if self._type == GRAPH_LINES or self._type == GRAPH_BOTH: 1031 if previous != None: 1032 context.move_to(previous[0], previous[1]) 1033 context.line_to(ax, ay) 1034 context.stroke() 1035 previous = (ax, ay) 1036 last = (ax, ay) 1037 else: 1038 previous = None 1039 1040 if self._fill_to != None: 1041 self._do_draw_fill(context, rect, xrange) 1042 1043 if self._show_title: 1044 self._do_draw_title(context, rect, last)
1045
1046 - def get_x_range(self):
1047 """ 1048 Get the the endpoints of the x interval. 1049 1050 @return: pair of numbers 1051 """ 1052 try: 1053 self._data.sort(lambda x, y: cmp(x[0], y[0])) 1054 return (self._data[0][0], self._data[-1][0]) 1055 except: 1056 return None
1057
1058 - def get_y_range(self):
1059 """ 1060 Get the the endpoints of the y interval. 1061 1062 @return: pair of numbers 1063 """ 1064 try: 1065 self._data.sort(lambda x, y: cmp(x[1], y[1])) 1066 return (self._data[0][1], self._data[-1][1]) 1067 except: 1068 return None
1069
1070 - def get_name(self):
1071 """ 1072 Get the name of the graph. 1073 1074 @return: string 1075 """ 1076 return self.get_property("name")
1077
1078 - def get_title(self):
1079 """ 1080 Returns the title of the graph. 1081 1082 @return: string 1083 """ 1084 return self.get_property("title")
1085
1086 - def set_title(self, title):
1087 """ 1088 Set the title of the graph. 1089 1090 @type title: string 1091 @param title: The graph's new title. 1092 """ 1093 self.set_property("title", title) 1094 self.emit("appearance_changed")
1095
1096 - def set_range_calc(self, range_calc):
1097 self._range_calc = range_calc
1098
1099 - def get_color(self):
1100 """ 1101 Returns the current color of the graph or COLOR_AUTO. 1102 1103 @return: a color (see set_color() for details). 1104 """ 1105 return self.get_property("color")
1106
1107 - def set_color(self, color):
1108 """ 1109 Set the color of the graph. color has to be a (r, g, b) triple 1110 where r, g, b are between 0 and 1. 1111 If set to COLOR_AUTO, the color will be choosen dynamicly. 1112 1113 @type color: a color 1114 @param color: The new color of the graph. 1115 """ 1116 self.set_property("color", color) 1117 self.emit("appearance_changed")
1118
1119 - def get_type(self):
1120 """ 1121 Returns the type of the graph. 1122 1123 @return: a type constant (see set_type() for details) 1124 """ 1125 return self.get_property("type")
1126
1127 - def set_type(self, type):
1128 """ 1129 Set the type of the graph to one of these: 1130 - GRAPH_POINTS: only show points 1131 - GRAPH_LINES: only draw lines 1132 - GRAPH_BOTH: draw points and lines, i.e. connect points with lines 1133 1134 @param type: One of the constants above. 1135 """ 1136 self.set_property("type", type) 1137 self.emit("appearance_changed")
1138
1139 - def get_point_size(self):
1140 """ 1141 Returns the radius of the data points. 1142 1143 @return: a poisitive integer 1144 """ 1145 return self.get_property("point_size")
1146
1147 - def set_point_size(self, size):
1148 """ 1149 Set the radius of the drawn points. 1150 1151 @type size: a positive integer in [1, 100] 1152 @param size: The new radius of the points. 1153 """ 1154 self.set_property("point_size", size) 1155 self.emit("appearance_changed")
1156
1157 - def get_fill_to(self):
1158 """ 1159 The return value of this method depends on the filling under 1160 the graph. See set_fill_to() for details. 1161 """ 1162 return self.get_property("fill-to")
1163
1164 - def set_fill_to(self, fill_to):
1165 """ 1166 Use this method to specify how the space under the graph should 1167 be filled. fill_to has to be one of these: 1168 1169 - None: dont't fill the space under the graph. 1170 - int or float: fill the space to the value specified (setting 1171 fill_to=0 means filling the space between graph and xaxis). 1172 - a Graph object: fill the space between this graph and the 1173 graph given as the argument. 1174 1175 The color of the filling is the graph's color with 30% opacity. 1176 1177 @type fill_to: one of the possibilities listed above. 1178 """ 1179 self.set_property("fill-to", fill_to) 1180 self.emit("appearance_changed")
1181
1182 - def get_fill_color(self):
1183 """ 1184 Returns the color that is used to fill space under the graph 1185 or COLOR_AUTO. 1186 """ 1187 return self.get_property("fill-color")
1188
1189 - def set_fill_color(self, color):
1190 """ 1191 Set which color should be used when filling the space under a 1192 graph. 1193 If color is COLOR_AUTO, the graph's color will be used. 1194 1195 @type color: a color or COLOR_AUTO. 1196 """ 1197 self.set_property("fill-color", color) 1198 self.emit("appearance_changed")
1199
1200 - def get_fill_opacity(self):
1201 """ 1202 Returns the opacity that is used to fill space under the graph. 1203 """ 1204 return self.get_property("fill-opacity")
1205
1206 - def set_fill_opacity(self, opacity):
1207 """ 1208 Set which opacity should be used when filling the space under a 1209 graph. The default is 0.3. 1210 1211 @type opacity: float in [0, 1]. 1212 """ 1213 self.set_property("fill-opacity", opacity) 1214 self.emit("appearance_changed")
1215
1216 - def get_show_values(self):
1217 """ 1218 Returns True if y values are shown. 1219 1220 @return: boolean 1221 """ 1222 return self.get_property("show-values")
1223
1224 - def set_show_values(self, show):
1225 """ 1226 Set whether the y values should be shown (only if graph type 1227 is GRAPH_POINTS or GRAPH_BOTH). 1228 1229 @type show: boolean 1230 """ 1231 self.set_property("show-values", show) 1232 self.emit("appearance_changed")
1233
1234 - def get_show_title(self):
1235 """ 1236 Returns True if the title of the graph is shown. 1237 1238 @return: boolean. 1239 """ 1240 return self.get_property("show-title")
1241
1242 - def set_show_title(self, show):
1243 """ 1244 Set whether to show the graph's title or not. 1245 1246 @type show: boolean. 1247 """ 1248 self.set_property("show-title", show) 1249 self.emit("appearance_changed")
1250
1251 - def add_data(self, data_list):
1252 """ 1253 Add data to the graph. 1254 1255 @type data_list: a list of pairs of numbers 1256 """ 1257 self._data += data_list 1258 self._range_calc.add_graph(self)
1259
1260 - def get_data(self):
1261 """ 1262 Returns the data of the graph. 1263 1264 @return: a list of x, y pairs. 1265 """ 1266 return self._data
1267 1268
1269 -def graph_new_from_function(func, xmin, xmax, graph_name, samples=100, do_optimize_sampling=True):
1270 """ 1271 Returns a line_chart.Graph with data created from the function 1272 y = func(x) with x in [xmin, xmax]. The id of the new graph is 1273 graph_name. 1274 The parameter samples gives the number of points that should be 1275 evaluated in [xmin, xmax] (default: 100). 1276 If do_optimize_sampling is True (default) additional points will be 1277 evaluated to smoothen the curve. 1278 1279 @type func: a function 1280 @param func: the function to evaluate 1281 @type xmin: float 1282 @param xmin: the minimum x value to evaluate 1283 @type xmax: float 1284 @param xmax: the maximum x value to evaluate 1285 @type graph_name: string 1286 @param graph_name: a unique name for the new graph 1287 @type samples: int 1288 @param samples: number of samples 1289 @type do_optimize_sampling: boolean 1290 @param do_optimize_sampling: set whether to add additional points 1291 1292 @return: line_chart.Graph 1293 """ 1294 delta = (xmax - xmin) / float(samples) 1295 data = [] 1296 x = xmin 1297 while x <= xmax: 1298 data.append((x, func(x))) 1299 x += delta 1300 1301 if do_optimize_sampling: 1302 data = optimize_sampling(func, data) 1303 1304 return Graph(graph_name, "", data)
1305
1306 -def optimize_sampling(func, data):
1307 new_data = [] 1308 prev_point = None 1309 prev_slope = None 1310 for x, y in data: 1311 if prev_point != None: 1312 if (x - prev_point[0]) == 0: return data 1313 slope = (y - prev_point[1]) / (x - prev_point[0]) 1314 if prev_slope != None: 1315 if abs(slope - prev_slope) >= 0.1: 1316 nx = prev_point[0] + (x - prev_point[0]) / 2.0 1317 ny = func(nx) 1318 new_data.append((nx, ny)) 1319 #print abs(slope - prev_slope), prev_point[0], nx, x 1320 prev_slope = slope 1321 1322 prev_point = x, y 1323 1324 if new_data: 1325 data += new_data 1326 data.sort(lambda x, y: cmp(x[0], y[0])) 1327 return optimize_sampling(func, data) 1328 else: 1329 return data
1330
1331 -def graph_new_from_file(filename, graph_name, x_col=0, y_col=1):
1332 """ 1333 Returns a line_chart.Graph with point taken from data file 1334 filename. 1335 The id of the new graph is graph_name. 1336 1337 Data file format: 1338 The columns in the file have to be separated by tabs or one 1339 or more spaces. Everything after '#' is ignored (comment). 1340 1341 Use the parameters x_col and y_col to control which columns to use 1342 for plotting. By default, the first column (x_col=0) is used for 1343 x values, the second (y_col=1) is used for y values. 1344 1345 @type filename: string 1346 @param filename: path to the data file 1347 @type graph_name: string 1348 @param graph_name: a unique name for the graph 1349 @type x_col: int 1350 @param x_col: the number of the column to use for x values 1351 @type y_col: int 1352 @param y_col: the number of the column to use for y values 1353 1354 @return: line_chart.Graph 1355 """ 1356 points = [] 1357 f = open(filename, "r") 1358 data = f.read() 1359 f.close() 1360 lines = data.split("\n") 1361 1362 for line in lines: 1363 line = line.strip() #remove special characters at beginning and end 1364 1365 #remove comments: 1366 a = line.split("#", 1) 1367 if a and a[0]: 1368 line = a[0] 1369 #get data from line: 1370 if line.find("\t") != -1: 1371 #col separator is tab 1372 d = line.split("\t") 1373 else: 1374 #col separator is one or more space 1375 #normalize to one space: 1376 while line.find(" ") != -1: 1377 line = line.replace(" ", " ") 1378 d = line.split(" ") 1379 d = filter(lambda x: x, d) 1380 d = map(lambda x: float(x), d) 1381 1382 points.append((d[x_col], d[y_col])) 1383 return Graph(graph_name, "", points)
1384