root/branch/ggpolo/umitInventory/TLGraph.py @ 1023

Revision 1023, 44.8 kB (checked in by ggpolo, 6 years ago)

Several updates for Timeline integration.

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