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

Revision 1030, 44.9 kB (checked in by ggpolo, 6 years ago)

Graph updates as data updates now.

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