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

Revision 1021, 43.7 kB (checked in by ggpolo, 6 years ago)

Changed how line_filter updates is changed and updated through several modules (cleaner and better way now), added CategoryFilter? for doing filter based on current graph display, added more functionaly to DataGrabber? and some other minor changes in ITG

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