root/branch/ggpolo/umitInventory/itg-integration/TLGraph.py @ 1020

Revision 1020, 41.4 kB (checked in by ggpolo, 6 years ago)

added more options for changes retrieve from database, GraphPreferences? now sets custom options, fixed ballon positioning on TLGraph

Line 
1# Copyright (C) 2007 Insecure.Com LLC.
2#
3# Authors: Guilherme Polo <ggpolo@gmail.com>
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18# USA
19
20import gtk
21import cairo
22import gobject
23
24"""
25ToDO: Change how balloon points are being handled.
26"""
27
28# testing colors
29colors = { 0: (0, 0.4, 1),
30           #0: (0.275, 0.627, 0.275),
31           1: (0.875, 0.259, 0.118),
32           2: (0.93, 0.83, 0),
33           3: (0.53, 0.52, 0.43),
34         }
35
36(VERTICAL, HORIZONTAL) = range(2)
37
38class InteractiveGraph(gtk.Widget):
39    """
40    Timeline Graph.
41    """
42
43    def __init__(self, data, start_points, max, y_label='', x_label='',
44                 vdiv_labels=[], hdivs=5, filter=None, balloons=True,
45                 draw_dashed_vert=False, draw_solid_vert=False,
46                 draw_arcs_always=False, draw_every_arc=False, 
47                 startup_animation=True, gradient_fill=False, 
48                 gradient_direction=VERTICAL, gradient_on_selection=False,
49                 progressive_selection_effect=True):
50        """
51        -> data is expected to be a dict wit the following format:
52       
53            { key0: [ (line1_firstpoint, line2_firstpoint, ..,
54                       lineN_firstpoint),
55                      (line1_secondpoint, line2_secondpoint, ..,
56                       lineN_secondpoint), .., (line1_nthpoint, ..,
57                      lineN_nthpoint) ],
58              key1: [ .. same as above .. ],
59               .
60               .
61              keyN: [ .. same as above .. ] }
62             
63            Number of keys in dict determines number of vertical divisors.
64                     
65           
66            Examples:
67                Graph with one line, three points break, one vertical divisor:
68               
69                  sample_data = { 0: [(1, ), (2, ), (3, )] }
70                 
71                  Note: Keys needs to be from 0 .. n (0, 1, 2, .., n)
72                 
73                Graph with two lines, two points breaks, two vertical divisors:
74                 
75                  sample_data = { 0: [(1, 2), (4, 5)],
76                                  1: [(10, 9), (3, 8)] }
77                                 
78            Bad example:
79                  sample_data = { 0: [(1, 2, 3), (1, 2)] }
80                 
81                Every line needs to contain same amount of points.               
82                 
83
84        -> start_points is a list of values that defines where each line
85            should start, it could be the last points from last date
86            before first date in this graph (for example).
87           
88            Example for graph with two lines:
89               start_values = (5, 0)
90               
91            Example for graph with one line:
92               start_values = (5, )
93               
94            If you especify more start points than needed, they will be
95            discarded.
96
97        -> max is the max value.
98
99        -> y_label defines a label for y axis.
100
101        -> x_label defines a label for x axis.
102
103        -> vdiv_labels defines labels to be used at each vertical mark.
104
105        -> hdivs determines the number of horizontal divisors.
106
107        -> filter is used to set what lines should be shown, format:
108
109            key -> True or False
110
111            key is an integer.
112           
113            Example: line_filter = { 0: True, 1: False, 2: True, 3: False }
114
115        -> balloons determines if we should draw balloons pointing to
116            higlighted points on selection.
117   
118        -> draw_dashed_vert determines if we should draw vertical dashed lines
119            on graph.
120           
121        -> draw_solid_vert determines if we should draw vertical solid lines
122           on graph.
123
124        -> draw_arcs_always determines if arcs should always be drawn, if this
125            is set to False arcs will be drawn only when a selection is done.
126       
127        -> draw_every_arc determines if every arc will be drawn or just
128            drawn at boundary points.
129                 
130        -> startup_anitmation determines if there will be an effect in graph
131            startup.
132
133        -> gradient_fill determines if selection will be painted with a
134            gradient or solid color.
135
136        -> gradient_direction determines the direction of gradient used when
137            painting selection (if gradient_fill == True).
138           
139        -> gradient_on_selection, fill selection with solid color or gradient
140            color.
141
142        -> progressive_selection_effect determines if there will be an effect
143            after selecting something or not.
144        """
145       
146        gtk.Widget.__init__(self)
147
148        # effects
149        self.speedup = False
150        self.bestvisual = False
151        self.standardmode = False
152        self.draw_dashed_vert = draw_dashed_vert
153        self.draw_solid_vert = draw_solid_vert
154        self.startup_animation = startup_animation
155        self.selection_effect = progressive_selection_effect
156        self.selection_gradient = gradient_on_selection
157        self.gradient_direction = gradient_direction
158        self.gradient_fill = gradient_fill
159        self.anim_timer = -1
160       
161        # points highlight (arcs)
162        self.draw_arcs_always = draw_arcs_always
163        self.draw_every_arc = draw_every_arc
164        self.show_balloons = balloons
165       
166        # borders
167        self.border_width = 2
168        self.border_fix = self.border_width / 2.0
169        self.top_reserved = 1 # will be calculated when realized.
170        self.left_reserved = 1 # will be calculated when realized.
171        self.bottom_reserved = 1 # will be calculated when realized.
172
173        # divisors
174        self.mark_size = 10 # | or - size
175        self.hdivisors = hdivs # number of horizontal divisors (min 2)
176        self.vdivisors = len(data) # number of vertical divisors (min 2)
177        self.hmarks_pos = { } # holds position for each horizontal mark
178        self.vmarks_pos = { } # holds position for each vertical mark
179
180        # selection
181        self.alpha_ts = 0 # alpha threshold for selection painting
182        self.selection_timer = - 1 # timer for doing selection painting
183        self.selection_painting = False # control for selection painting
184
185        # graph
186        self.selection = -1 # nothing selected
187        self.hover = -1 # nothing being hovered
188        self.__filter = None
189        self.line_filter = filter
190        self.max = max
191        self.ylabel = y_label
192        self.xlabel = x_label
193        self.vdiv_labels = vdiv_labels
194        self.start_pts_data = start_points
195        self.start_pts = [ ]
196        self.graph_data = data
197        self.pts_per_div = len(self.graph_data[0])
198        self.num_graph_lines = -1
199        self.graphpoints = { }
200        self.graph = { } # holds all data necessary for drawing the graph.
201        self.graph["fg_black"] = (0, 0, 0)
202        self.graph["fg_color"] = (0, 0, 0, 0.5)
203        self.graph["bg_gradient"] = ((0.729, 0.851, 1, 0.8), (1, 1, 1, 0.6))
204        self.graph["gradient_sel"] = ((0, 0.4, 1), (1, 1, 1))
205        self.graph["bg_solid"] = (0.816, 0.906, 1)
206        self.graph["bg_selection"] = (0, 0.4, 1)
207        self.graph["selection_alpha_max"] = 0.25
208
209        # graph drawing animation
210        self.divisors = 2 # 1
211        self.ccount = 0
212        self.cur_point_indx = 0
213        self.painting_piece = 0
214
215        # balloons
216        self.balloons = { }
217 
218
219    def speedup_performance(self, *args):
220        """
221        Change settings to speed up drawing performance.
222        """
223        if self.speedup:
224            return # already set speedup mode
225       
226        self.hdivisors = 3
227        self.gradient_fill = False
228        self.selection_gradient = False
229        self.selection_effect = False
230        self.draw_dashed_vert = False
231        self.draw_solid_vert = False
232        self.draw_arcs_always = False
233        self.draw_every_arc = False
234        self.show_balloons = True
235        self.anim_timer = 0
236       
237        self.speedup = True
238        self.bestvisual = False
239        self.standardmode = False
240
241        self.setup_new_graph()
242
243
244    def best_visual(self, *args):
245        """
246        Change settings to draw with best visual.
247        """
248        if self.bestvisual:
249            return # already set bestvisual mode
250       
251        self.hdivisors = 5
252        self.gradient_fill = True
253        self.selection_gradient = True
254        self.selection_effect = True
255        self.draw_dashed_vert = True
256        self.draw_solid_vert = False
257        self.draw_arcs_always = False
258        self.draw_every_arc = True
259        self.show_balloons = True
260       
261        self.speedup = False
262        self.bestvisual = True
263        self.standardmode = False
264       
265        self.setup_new_graph()
266               
267   
268    def standard_mode(self, *args):
269        """
270        Set standard settings.
271        """
272        if self.standardmode:
273            return # already set standard mode
274       
275        self.hdivisors = 5
276        self.gradient_fill = False
277        self.selection_gradient = False
278        self.selection_effect = True
279        self.draw_dashed_vert = True
280        self.draw_solid_vert = False
281        self.draw_arcs_always = False
282        self.draw_every_arc = False
283        self.show_balloons = True
284       
285        self.speedup = False
286        self.bestvisual = False
287        self.standardmode = True
288   
289        # test for restarting animation
290        #self.startup_animation = True
291        #self.anim_timer = -1
292        #self.ccount = 0
293        #self.cur_point_indx = 0
294        #self.painting_piece = 0
295        #self._calculate_graph_points()
296        # works!
297
298        self.setup_new_graph()
299
300
301    def setup_new_graph(self):
302        """
303        Do everything necessary after changing graph attributes.
304        """
305        self.hmarks_pos = { }
306        self._calculate_graph_alloc()
307
308        if self.flags() & gtk.REALIZED:
309            self.queue_draw()
310
311
312    def get_start_effect(self):
313        """
314        Retruns True if widget should do an effect on graph startup.
315        """
316        return self.__seffect
317
318
319    def set_start_effect(self, effect):
320        """
321        Changes startup_animation value and also set new events based on this
322        value.
323        """
324        self.__seffect = effect
325       
326        if not self.startup_animation and (self.flags() & gtk.REALIZED):
327            self.add_events(gtk.gdk.POINTER_MOTION_MASK |
328                            gtk.gdk.ENTER_NOTIFY_MASK |
329                            gtk.gdk.LEAVE_NOTIFY_MASK)
330
331
332    def get_active_filter(self):
333        """
334        Returns current filter being used to draw lines.
335        """
336        return self.__filter
337
338
339    def set_active_filter(self, filter):
340        """
341        Set filter to be used when drawing lines.
342        """
343        prev_filter = self.__filter
344        if prev_filter != filter:
345            self.__filter = filter
346
347            if self.flags() & gtk.REALIZED:
348                self.queue_draw()
349
350
351    def get_alpha_threshold(self):
352        """
353        Get alpha to be used in selection painting.
354        """
355        return self.__alphats
356
357
358    def set_alpha_threshold(self, threshold):
359        """
360        Set alpha to be used in selection painting.
361        """
362        self.__alphats = threshold
363
364
365    def get_hdivisors(self):
366        """
367        Get number of horizontal divisors
368        """
369        return self.__hdiv
370
371
372    def set_hdivisors(self, hdiv):
373        """
374        Set number of horizontal divisors.
375        """
376        if hdiv < 2: # min is two
377            hdiv = 2
378
379        self.__hdiv = hdiv - 1
380
381
382    def _calculate_graph_alloc(self):
383        """
384        Calculates x space, y space, distance between horizontal and
385        vertical divisors for graph for current allocated space.
386        """
387        self.graph["x_space"] = self.allocation[2] - self.left_reserved - \
388                                self.border_width
389        self.graph["y_space"] = self.allocation[3] - (self.top_reserved + \
390                                                      self.bottom_reserved + \
391                                                      self.border_fix)
392
393        self.graph["hdiv"] = (self.graph["y_space"] - self.border_width) / \
394                             float(self.hdivisors)
395        self.graph["vdiv"] = (self.graph["x_space"] - self.border_width) / \
396                             float(self.vdivisors)
397
398
399    def _calculate_border_reserved(self):
400        """
401        Calculate space needed to write labels.
402        """
403        cr = self.window.cairo_create()
404        _, _, width, height, _, _ = cr.text_extents("%d" % self.max)
405        self.left_reserved = width + 4 + self.mark_size
406
407        if width / 2.0 > self.top_reserved:
408            self.top_reserved = width / 2.0
409
410        if self.ylabel:
411            _, _, _, height, _, _ = cr.text_extents(self.ylabel)
412            self.left_reserved += height + 6
413
414        self.graph["x_start"] = self.left_reserved
415        self.graph["y_start"] = self.top_reserved
416
417        # height will just work for labels with numbers here, will need
418        # to change this asap. (likely to break with letter 'J')
419        self.bottom_reserved = height + 4 + self.mark_size
420
421        if self.xlabel:
422            _, _, _, height, _, _ = cr.text_extents(self.xlabel)
423            self.bottom_reserved += height + 6
424           
425       
426    def _calculate_point_ypos(self, value):
427        """
428        Caluate y position for a value.
429        """
430        if value == 0:
431            return self.graph["y_space"] + self.top_reserved
432        else:
433            return self.graph["y_space"] + self.top_reserved \
434                   - (self.graph["y_space"] / (self.max / float(value)))
435
436
437    def _calculate_graph_points(self):
438        """
439        Calculates (x, y) points for data in self.graph_data.
440        """
441        x_pts_dist = self.graph["vdiv"] / self.pts_per_div
442        x_pts_dx = self.left_reserved + self.border_fix + x_pts_dist
443
444        # Calculate data points position
445        for key, item in self.graph_data.items():
446            k_pts = [ ]
447            for indx, piece in enumerate(item):
448                # calculate y position
449                pt = [ ]
450                for pv in piece:
451                    pt.append(self._calculate_point_ypos(pv))
452                   
453                # add x position to the point
454                pt = [(x_pts_dx, p) for p in pt]
455
456                k_pts.append(pt)
457                x_pts_dx += x_pts_dist
458
459            self.graphpoints[key] = k_pts
460       
461        self.num_graph_lines = len(self.graphpoints[0][0])
462           
463        # Calculate start_point(s) position
464        start_x = self.left_reserved
465        start_y = [ ]
466        for pv in self.start_pts_data:
467            start_y.append(self._calculate_point_ypos(pv))
468       
469        self.start_pts = [(start_x, y) for y in start_y]
470
471        # set startup points for graph animation
472        self.cur_point = { }
473        for i in range(self.num_graph_lines):
474            self.cur_point[i] = self.start_pts[i]
475           
476
477    def _gradient_fill(self, cr, gradient=True):
478        """
479        Fill graph with gradient or solid color.
480        """
481        cr.save()
482
483        cr.rectangle(self.graph["x_start"] + self.border_fix,
484                     self.graph["y_start"] + self.border_fix,
485                     self.graph["x_space"] - self.border_fix,
486                     self.graph["y_space"] - self.border_fix)
487
488        if gradient: # gradient fill
489            if self.gradient_direction == VERTICAL:
490                end_x = self.graph["x_start"] - self.border_width
491                end_y = self.graph["y_space"] - self.border_width
492            else:
493                end_x = self.graph["x_space"] - self.border_width
494                end_y = self.graph["y_start"] - self.border_width
495
496            pat = cairo.LinearGradient(self.graph["x_start"] + self.border_fix, 
497                                       self.graph["y_start"] + self.border_fix, 
498                                       end_x, end_y)
499            pat.add_color_stop_rgba(0, *self.graph["bg_gradient"][0])
500            pat.add_color_stop_rgba(1, *self.graph["bg_gradient"][1])
501            cr.set_source(pat)
502
503        else: # solid fill
504            cr.set_source_rgb(*self.graph["bg_solid"])
505
506        cr.fill()
507
508        cr.restore()
509
510
511    def _solid_fill(self, cr):
512        """
513        Fill graph with solid coloring.
514        """
515        self._gradient_fill(cr, False)
516
517
518    def _draw_graph_base(self, cr):
519        """
520        Draw base of graph.
521        """       
522        # draw x, y axis and store marker positions
523        #  |
524        # _|______
525        #  |
526        cr.save()
527
528        cr.move_to(self.graph["x_start"], self.graph["y_start"])
529        cr.rel_line_to(0, self.graph["y_space"] + self.mark_size)
530
531        self.vmarks_pos[0] = cr.get_current_point()
532
533        cr.rel_line_to(0, - self.mark_size)
534        cr.rel_line_to(- self.mark_size, 0)
535
536        self.hmarks_pos[0] = cr.get_current_point()
537
538        cr.rel_line_to(self.graph["x_space"] + self.mark_size, 0)
539
540        cr.set_source_rgba(*self.graph["fg_color"])
541        cr.stroke()
542
543        cr.restore()
544
545        # horizontal divisors (first is draw when x axis was drawn)
546        hdiv_y = self.top_reserved
547
548        cr.save()
549
550        for hdiv in range(self.hdivisors):
551            # + 0.5 is used below to draw a sharp line
552            cr.move_to(self.graph["x_start"] - self.mark_size - \
553                       self.border_fix, int(hdiv_y) + 0.5)
554
555            cpoint = list(cr.get_current_point())
556            cpoint[0] += self.border_fix
557            self.hmarks_pos[self.hdivisors - hdiv] = cpoint
558
559            cr.rel_line_to(self.graph["x_space"] + self.mark_size, 0)
560           
561            hdiv_y += self.graph["hdiv"]
562
563        cr.set_source_rgba(*self.graph["fg_color"])
564        cr.set_line_width(0.5)
565        cr.stroke()
566
567        cr.restore()
568
569        # draw vertical marks at bottom (first is draw when y axis was drawn)
570        vdiv_x = self.graph["x_space"] + self.left_reserved - \
571                 self.border_fix - self.graph["vdiv"]
572
573        cr.save()
574
575        cr.set_source_rgba(*self.graph["fg_color"])
576
577        for vdiv in range(1, self.vdivisors):
578            # dashed vertical line
579            if self.draw_dashed_vert or self.draw_solid_vert:
580                cr.move_to(vdiv_x, self.top_reserved + self.border_fix)
581                if self.draw_dashed_vert:
582                    cr.set_dash([1, 2], 0)
583                cr.rel_line_to(0, self.graph["y_space"])
584                cr.stroke()
585                cr.set_dash([])
586
587            # bottom vertical mark
588            cr.move_to(vdiv_x, self.top_reserved + self.border_fix + \
589                               self.graph["y_space"])
590            cr.rel_line_to(0, self.mark_size)
591            cpoint = list(cr.get_current_point())
592
593            if self.draw_dashed_vert or self.draw_solid_vert:
594                cr.stroke()
595
596            cpoint[1] -= self.border_fix
597            self.vmarks_pos[self.vdivisors - vdiv] = cpoint
598
599            vdiv_x -= self.graph["vdiv"]
600       
601        if not self.draw_dashed_vert or not self.draw_solid_vert:
602            cr.stroke()
603
604        cr.restore()
605
606   
607    def _write_hmarks_values(self, cr):
608        """
609        Write horizontal marks values based on self.max
610        """
611        self.hm_value = self.max / float(self.hdivisors)
612        self.hm_cur = self.hm_value
613
614        cr.save()
615        cr.set_source_rgba(*self.graph["fg_color"])
616
617        for key, pos in self.hmarks_pos.items():
618            if key == 0:
619                # first value is 0
620                text = "0"
621            elif key == self.hdivisors:
622                # last value is max
623                text = "%d" % self.max
624            else:
625                text = "%d" % self.hm_cur
626                self.hm_cur += self.hm_value
627
628            # move to correct position
629            _, _, width, height, _, _ = cr.text_extents(text)
630            cr.move_to(pos[0] - width - 4, pos[1] + height / 2.0)
631
632            # write text
633            cr.show_text(text)
634       
635        cr.restore()
636
637
638    def _write_vmarks_values(self, cr):
639        """
640        Write vertical marks labels. (using key number for now).
641        """
642        cr.save()
643
644        cr.set_source_rgba(*self.graph["fg_color"])
645
646        for key, pos in self.vmarks_pos.items():
647            try:
648                text = self.vdiv_labels[key]
649            except IndexError:
650                text = "NA" # "Not Available"
651
652            _, _, width, height, _, _ = cr.text_extents(text)
653
654            cr.move_to(pos[0] - (width / 2.0) - self.border_fix,
655                       pos[1] + 8)
656
657            cr.show_text(text)
658
659        cr.restore()
660
661
662    def _write_axis_labels(self, cr):
663        """
664        Write x, y axis labels.
665        """
666        if self.ylabel:
667            cr.save()
668            _, _, width, height, _, _ = cr.text_extents(self.ylabel)
669            cr.move_to(height + 2, (self.graph["y_space"] / 2.0) + \
670                                   width / 2.0 + self.top_reserved + \
671                                   self.border_width)
672            cr.rotate(- 3.14/2)
673            cr.show_text(self.ylabel)
674            cr.restore()
675
676        if self.xlabel:
677            cr.save()
678            _, _, width, height, _, _ = cr.text_extents(self.xlabel)
679            cr.move_to((self.graph["x_space"] / 2.0) - (width / 2.0) + \
680                       self.left_reserved,
681                       self.allocation[3] - height/2.0)
682            cr.show_text(self.xlabel)
683            cr.restore()
684
685
686    def _paint_hover_area(self, cr):
687        """
688        Paint area being hovered.
689        """
690        start_x = self.left_reserved + (self.hover * self.graph["vdiv"])
691        start_y = self.top_reserved + self.graph["y_space"]
692        width = self.graph["vdiv"] + self.border_fix
693        height = self.bottom_reserved / 4.0
694
695        cr.save()
696
697        cr.rectangle(start_x, start_y, width, height)
698        if self.speedup:
699            cr.set_source_rgb(*self.graph["fg_black"])
700        else:
701            cr.set_source_rgba(*self.graph["fg_color"])
702        cr.fill()
703
704        cr.restore()
705
706
707    def _paint_selection(self, cr, alpha_threshold=None):
708        """
709        Paint selected area.
710        """
711        if self.selection == -1:
712            return
713
714        start_x = self.left_reserved + \
715                  (self.selection * self.graph["vdiv"]) + self.border_fix
716        start_y = self.top_reserved
717        height = self.graph["y_space"]
718        width = self.graph["vdiv"]
719
720        cr.save()
721
722        cr.rectangle(start_x, start_y, width, height)
723       
724        if self.selection_gradient: # use gradient
725            pat = cairo.LinearGradient(start_x, start_y, start_x, height)
726           
727            bottom_color = self.graph["gradient_sel"][0][:3]
728            upper_color = self.graph["gradient_sel"][1][:3]
729           
730            if not alpha_threshold:
731                alpha_threshold = self.graph["selection_alpha_max"]
732           
733            pat.add_color_stop_rgba(0, bottom_color[0], bottom_color[1],
734                                    bottom_color[2], alpha_threshold)
735            pat.add_color_stop_rgba(1, upper_color[0], upper_color[1],
736                                    upper_color[2], alpha_threshold)
737            cr.set_source(pat)
738       
739        else: # use solid color
740            color = list(self.graph["bg_selection"])
741            if not alpha_threshold:
742                alpha_threshold = self.graph["selection_alpha_max"]
743       
744            color.extend([alpha_threshold, ])
745
746            cr.set_source_rgba(*color)
747           
748        cr.fill()
749
750        cr.restore()
751
752
753    def _progressive_draw_timer(self):
754        """
755        This method handles graph animation effect.
756        """
757        # check if we competed a line segment
758        if self.painting_piece == self.divisors:
759            self.painting_piece = 0
760
761            # start to draw a new line segment then =)
762            self.cur_point_indx += 1
763
764            # check if we drawn all points in a vertical divisor
765            if self.cur_point_indx == len(self.graphpoints[0]):
766               
767                # start to draw at new vertical divisor
768                self.ccount += 1
769                # at it's first point
770                self.cur_point_indx = 0
771
772        cr = self.window.cairo_create()
773
774        ret = self._draw_by_piece(cr)
775
776        # check if we are done
777        if not ret:
778            print 'Animation completed.'
779            #self.anim_timer = -1
780            self.startup_animation = False
781            return False
782
783        return True
784
785
786    def lines_to_draw(self):
787        """
788        Returns number of "kind" of lines that will be drawn.
789        """
790        if self.line_filter:
791            return sum(1 for k, v in self.line_filter.items() if v == True)
792        else:
793            return self.num_graph_lines
794
795
796    def _draw_by_piece(self, cr):
797        """
798        Draw graph by pieces to create a nice visual effect for startup or
799        when data changes.
800        """
801        cr.set_line_width(2.5)
802        cr.set_line_join(cairo.LINE_JOIN_ROUND)
803
804        for k in range(self.num_graph_lines):
805           
806            # check for final point           
807            if self.ccount == len(self.graph_data):
808                # could do a check to see if we are using area graph and then
809                # paint the background using a timer.
810               
811                return False
812           
813
814            # check if we should draw this line.
815            if self.line_filter and not self.line_filter[k]:
816                continue
817
818            # get previous point
819            if self.ccount == 0 and self.cur_point_indx == 0:
820                p = self.start_pts[k]
821            elif self.ccount != 0 and self.cur_point_indx == 0:
822                p = self.graphpoints[self.ccount - 1]\
823                                    [len(self.graphpoints[0])-1][k]
824            else:
825                p = self.graphpoints[self.ccount][self.cur_point_indx - 1][k]
826
827            # get current point
828            cp = self.graphpoints[self.ccount][self.cur_point_indx][k]
829
830            cr.save()
831            cr.set_source_rgb(*colors[k])
832
833            cr.move_to(*self.cur_point[k])
834
835            # draw a piece of line segment
836            cr.rel_line_to((cp[0] - p[0]) / float(self.divisors), 
837                           (cp[1] - p[1]) / float(self.divisors))
838
839            # store current point
840            self.cur_point[k] = cr.get_current_point()
841
842            # check if we should draw a circle
843            if self.draw_arcs_always:
844                if self.draw_every_arc:
845                    if self.painting_piece == (self.divisors - 2):
846                        cr.arc(cp[0], cp[1], 2, 0, 2 * 3.14)
847                        cr.fill_preserve()
848                       
849                elif self.cur_point_indx == self.pts_per_div - 1 and \
850                    self.painting_piece == (self.divisors - 2):
851                   
852                    cr.arc(cp[0], cp[1], 2, 0, 2 * 3.14)
853                    cr.fill_preserve()
854           
855            # especial check for arc at startup point
856            if self.ccount == 0 and self.draw_arcs_always and \
857                self.cur_point_indx == 0 and self.painting_piece == 0:
858               
859                cr.move_to(*self.start_pts[k])
860                cr.arc(self.start_pts[k][0], 
861                        self.start_pts[k][1], 2, 0, 2 * 3.14)
862                cr.fill_preserve()
863           
864            cr.stroke()
865
866            cr.restore()
867
868        self.painting_piece += 1
869       
870        return True
871
872
873    def _draw_graph(self, cr):
874        """
875        Draw graph, connecting points using rel_line_to.
876        """
877        # get startup points
878        cur_points = { }
879       
880        for i in range(self.num_graph_lines):
881            cur_points[i] = self.start_pts[i]
882           
883        cr.set_line_width(2.5)
884        cr.set_line_join(cairo.LINE_JOIN_ROUND)
885
886        for indx in range(self.num_graph_lines):
887            # check if we should draw this line
888            if self.line_filter and not self.line_filter[indx]:
889                continue
890
891            for key, pts in self.graphpoints.items():
892                for pind, pt in enumerate(pts):
893
894                    # get previous point
895                    if key == 0 and pind == 0:
896                        p = cur_points[indx]
897                    elif key != 0 and pind == 0:
898                        p = self.graphpoints[key - 1] \
899                                            [len(self.graphpoints[0]) - 1] \
900                                            [indx]
901                    else:
902                        p = self.graphpoints[key][pind - 1][indx]
903
904                    # get current point
905                    cp = self.graphpoints[key][pind][indx]
906   
907                    cr.save()
908                    cr.set_source_rgb(*colors[indx])
909
910                    cr.move_to(*cur_points[indx])
911                   
912                    # draw line
913                    cr.rel_line_to(cp[0] - p[0], cp[1] - p[1])
914
915                    # get new "startup" point
916                    cur_points[indx] = cr.get_current_point()
917
918                    # check if we should draw a circle now.
919                    if self.draw_arcs_always:
920                        if self.draw_every_arc:
921                            cr.arc(cp[0], cp[1], 2, 0, 2 * 3.14)
922                            cr.fill_preserve()
923                        elif pind == self.pts_per_div - 1:
924                            cr.arc(cp[0], cp[1], 2, 0, 2 * 3.14)
925                            cr.fill_preserve()
926                           
927                    else:
928                        if self.draw_every_arc:
929                            if self.selection == key or \
930                                (self.selection == key + 1 and pind == \
931                                 self.pts_per_div - 1):
932                                cr.arc(cp[0], cp[1], 2, 0, 2 * 3.14)
933                                cr.fill_preserve() 
934                               
935                        elif pind == self.pts_per_div - 1 and \
936                            (self.selection == key or \
937                             (self.selection == key + 1)):
938                           
939                            cr.arc(cp[0], cp[1], 2, 0, 2 * 3.14)
940                            cr.fill_preserve()
941                   
942                    # especial check for arc at startup point
943                    if key == 0 and pind == 0 and (self.selection == 0 or \
944                                                   self.draw_arcs_always):
945   
946                        cr.move_to(*self.start_pts[indx])
947                        cr.arc(self.start_pts[indx][0], 
948                               self.start_pts[indx][1], 2, 0, 2 * 3.14)
949                        cr.fill_preserve()
950                   
951                   
952                    cr.stroke()
953                    cr.restore()
954         
955
956    def _draw_area_graph(self, cr):
957        """
958        Draw graph, connecting points and filling the area.
959        """
960        cr.save()
961        cr.set_line_join(cairo.LINE_JOIN_ROUND)
962
963        # connect the points, go! =)
964        for indx, ini_p in enumerate(self.start_pts):
965            if self.line_filter and not self.line_filter[indx]:
966                continue
967
968            cr.new_path()
969            cr.move_to(*ini_p)
970
971            for key, pts in self.graphpoints.items():
972                for p in pts:
973                    cr.line_to(*p[indx])
974       
975            x = cr.get_current_point()[0]
976
977            # stroke every lines.
978            cr.set_source_rgb(*colors[indx])
979            cr.stroke_preserve()
980
981            # connect end with start point.
982            cr.line_to(x, self.graph["y_space"] + self.top_reserved)
983            cr.line_to(self.left_reserved + self.border_fix,
984                       self.graph["y_space"] + self.top_reserved)
985            cr.close_path()
986
987            # then set color, and fill.
988            cr.set_source_rgba(colors[indx][0], colors[indx][1], 
989                               colors[indx][2], 0.5)
990            cr.fill()
991
992        cr.restore()
993
994
995    def _draw_balloon(self, cr, text, color, pointing_pt):
996        """
997        Draws a balloon with some text pointing to pointing_pt.
998        """
999        # widget total size
1000        t_width = self.allocation[2]
1001        t_height = self.allocation[3]
1002
1003        # calculate balloon size
1004        _, _, width, height, _, _ = cr.text_extents(text)
1005        bwidth = width + 8
1006        bheight = height + 6
1007
1008        dir_x = 5
1009        dir_y = 8
1010        w = bwidth
1011        h = bheight
1012       
1013        control = { (True, True): (dir_x, dir_y, w, h, -2, 5, 
1014                                    8, 8 + height),
1015                    (True, False): (dir_x, -dir_y, w, -h, 2, 5, 
1016                                    8, -height),
1017                    (False, True): (-dir_x, dir_y, -w, h, -2, -5, 
1018                                    -width - 10, height + 8),
1019                    (False, False): (-dir_x, -dir_y, -w, -h, 2, -5, 
1020                                     -width -10, -8)
1021        }
1022
1023        coords = control[(pointing_pt[0] < width, 
1024                          pointing_pt[1] - (bheight + 4) < 0)]
1025
1026        # draw balloon
1027        cr.save()
1028        cr.new_path()
1029
1030        cr.move_to(*pointing_pt)
1031        cr.rel_line_to(coords[0], coords[1])
1032        cr.rel_line_to(0, coords[3] + coords[4])
1033        cr.rel_line_to(coords[2], 0)
1034        cr.rel_line_to(0, - coords[3] + coords[4])
1035        cr.rel_line_to(- coords[2] + coords[5], 0)
1036
1037        # fill balloon
1038        cr.set_source_rgba(color[0], color[1], color[2], 0.5)
1039        cr.fill()
1040        cr.restore()
1041
1042        # write text
1043        cr.move_to(pointing_pt[0] + coords[6], pointing_pt[1] + coords[7])
1044        cr.show_text(text)
1045
1046
1047    def _update_alpha_ts(self):
1048        """
1049        Update alpha to be used in selection painting.
1050        """
1051        self.alpha_ts += 0.05
1052
1053        if self.alpha_ts > self.graph["selection_alpha_max"]:
1054            self.selection_painting = False
1055            self.alpha_ts = 0
1056           
1057            self.queue_draw()
1058
1059            return False
1060
1061        self.queue_draw()
1062
1063        return True
1064
1065
1066    def _setup_balloons(self):   
1067        """
1068        Get bound points for current selection.
1069        """
1070        self.balloons = { }
1071        if not self.show_balloons:
1072            # Nothing to do here, balloons disabled.
1073            return
1074       
1075        # point position needs to be calculated inside balloon creation!
1076        if self.selection != -1:           
1077            count = 0
1078            for pts in self.graphpoints[self.selection]:
1079
1080                for indx, pt in enumerate(pts):
1081                    if self.line_filter and not self.line_filter[indx]:
1082                        continue
1083               
1084                    if not self.draw_every_arc and \
1085                        count != self.pts_per_div - 1:
1086                        continue
1087               
1088                    color = colors[indx]
1089                    value = self.graph_data[self.selection][count][indx]
1090                    self.balloons[pt] = (value, color)
1091
1092                count += 1
1093
1094            # get left boundary points
1095            lines = len(self.graphpoints[self.selection][0])
1096            points_per_div = len(self.graphpoints[self.selection])
1097            for indx in range(lines):
1098                if self.line_filter and not self.line_filter[indx]:
1099                        continue
1100                   
1101                color = colors[indx]
1102               
1103                # startup point
1104                if self.selection == 0:
1105                    pt = self.start_pts[indx]     
1106                    value = self.start_pts_data[indx]
1107                # somewhere else in the graph
1108                else:
1109                    pt = self.graphpoints[self.selection - 1] \
1110                                         [points_per_div - 1][indx]
1111                    value = self.graph_data[self.selection - 1] \
1112                                         [points_per_div - 1][indx]
1113                   
1114                self.balloons[pt] = (value, color)
1115                       
1116
1117    def do_realize(self):
1118        """
1119        Setup widget and calls methods for calculating everything needed
1120        to draw graph.
1121        """
1122        self.set_flags(self.flags() | gtk.REALIZED | gtk.CAN_FOCUS)
1123
1124        self.window = gtk.gdk.Window(self.get_parent_window(),
1125                                     width=self.allocation.width,
1126                                     height=self.allocation.height,
1127                                     window_type=gtk.gdk.WINDOW_CHILD,
1128                                     wclass=gtk.gdk.INPUT_OUTPUT,
1129                                     event_mask=self.get_events() | 
1130                                            gtk.gdk.EXPOSURE_MASK |
1131                                            gtk.gdk.BUTTON_PRESS_MASK |
1132                                            gtk.gdk.BUTTON_RELEASE_MASK )
1133
1134        self.window.set_user_data(self)
1135        self.style.attach(self.window)
1136        self.style.set_background(self.window, gtk.STATE_NORMAL)
1137        self.window.move_resize(*self.allocation)
1138
1139        self.startup_animation = self.startup_animation
1140
1141        self._calculate_border_reserved()
1142        self._calculate_graph_alloc()
1143        self._calculate_graph_points()
1144
1145
1146    def do_unrealize(self):
1147        self.window.set_user_data(None)
1148
1149
1150    def do_size_request(self, requisition):
1151        """
1152        Sets an acceptable minimal size.
1153        """
1154        # minimal size
1155        #requisition.width = 70 * 3.2
1156        #requisition.height = 70
1157
1158        # optimal size
1159        width, _ = gtk.gdk.get_default_root_window().get_size()
1160        requisition.width = (width * 3) / 4
1161        requisition.height = 140
1162
1163
1164    def do_size_allocate(self, allocation):
1165        """
1166        Handles size allocation, calculate new points positions and
1167        graph dimensions.
1168        """
1169        self.allocation = allocation
1170
1171        if self.flags() & gtk.REALIZED:
1172            self.window.move_resize(*allocation)
1173
1174        self._calculate_graph_alloc()
1175        self._calculate_graph_points()
1176        self._setup_balloons()
1177
1178
1179    def do_motion_notify_event(self, event):
1180        """
1181        Handles mouse motion.
1182        """
1183        prev_status = self.hover
1184
1185        if self.top_reserved <= event.y <= self.graph["y_space"] + \
1186        self.top_reserved and self.left_reserved <= event.x <= \
1187        self.graph["x_space"] + self.left_reserved:
1188
1189            new_status = int((event.x - self.left_reserved) / \
1190                         self.graph["vdiv"])
1191
1192            if new_status >= len(self.graph_data):
1193                return
1194
1195            self.hover = new_status
1196        else:
1197            self.hover = -1
1198
1199        if prev_status != self.hover:
1200            self.queue_draw()
1201
1202
1203    def do_button_release_event(self, event):
1204        """
1205        Handles mouse button release.
1206        """
1207        if event.button == 1:
1208            prev_state = self.selection
1209            self.balloons = { }
1210
1211            if self.hover != -1:
1212                if self.selection == self.hover:
1213                    self.selection = -1
1214                else:
1215                    self.selection = self.hover
1216           
1217            if prev_state != self.selection:
1218                self.selection_timer = -1
1219                gobject.source_remove(self.selection_timer)
1220                self.queue_draw()
1221
1222
1223    def do_expose_event(self, event):
1224        """
1225        Draws graph.
1226        """
1227        cr = self.window.cairo_create()
1228        cr.rectangle(*event.area)
1229        cr.clip()
1230
1231        # white background
1232        cr.save()
1233        cr.rectangle(*event.area)
1234        cr.set_source_rgb(1, 1, 1)
1235        cr.fill()
1236        cr.restore()
1237
1238        # graph background
1239        if self.gradient_fill:
1240            self._gradient_fill(cr)
1241        else:
1242            self._solid_fill(cr)
1243
1244        # graph base, axis
1245        self._draw_graph_base(cr)
1246
1247        # write horizontal marks labels
1248        self._write_hmarks_values(cr)
1249
1250        # write vertical marks labels
1251        self._write_vmarks_values(cr)
1252
1253        # write axis labels
1254        self._write_axis_labels(cr)
1255
1256        # paint selection
1257        if self.selection_painting:
1258            self._paint_selection(cr, self.alpha_ts)
1259        else:
1260            if self.selection != -1:
1261                if self.selection_timer == -1 and self.selection_effect:
1262                    self.selection_timer = gobject.timeout_add(20,
1263                                                        self._update_alpha_ts)
1264                    self.selection_painting = True
1265                else:
1266                    self._setup_balloons()
1267                    self._paint_selection(cr)
1268
1269        # paint area being hovered
1270        if -1 < self.hover < len(self.graph_data):
1271            self._paint_hover_area(cr)
1272
1273        # draw graph
1274        if self.startup_animation and self.anim_timer == -1:
1275            # start animation
1276            self.anim_timer = gobject.timeout_add(20, 
1277                                                  self._progressive_draw_timer)
1278        elif not self.startup_animation:
1279            self._draw_graph(cr)
1280            #self._draw_area_graph(cr)
1281
1282        # draw balloons
1283        for pt, props in self.balloons.items():
1284            self._draw_balloon(cr, "%d" % props[0], props[1], pt)
1285
1286
1287    # Properties
1288    startup_animation = property(get_start_effect, set_start_effect)
1289    line_filter = property(get_active_filter, set_active_filter)
1290    alpha_ts = property(get_alpha_threshold, set_alpha_threshold)
1291    hdivisors = property(get_hdivisors, set_hdivisors)
1292
1293
1294gobject.type_register(InteractiveGraph)
1295
Note: See TracBrowser for help on using the browser.