billreminder r643 - in trunk: . po src/gui src/gui/widgets src/lib



Author: ogmaciel
Date: Tue Sep 23 20:53:45 2008
New Revision: 643
URL: http://svn.gnome.org/viewvc/billreminder?rev=643&view=rev

Log:
New widgets added to support charting and new calendaring.

Added:
   trunk/src/gui/widgets/calendarwidget.py
   trunk/src/gui/widgets/charting.py
   trunk/src/gui/widgets/chartwidget.py
Modified:
   trunk/ChangeLog
   trunk/po/POTFILES.in
   trunk/src/gui/maindialog.py
   trunk/src/lib/actions.py
   trunk/src/lib/dbus_actions.py

Modified: trunk/po/POTFILES.in
==============================================================================
--- trunk/po/POTFILES.in	(original)
+++ trunk/po/POTFILES.in	Tue Sep 23 20:53:45 2008
@@ -20,6 +20,7 @@
 src/gui/categoriesdialog.py
 src/gui/maindialog.py
 src/gui/prefdialog.py
+src/gui/widgets/calendarwidget.py
 src/gui/widgets/datebutton.py
 src/gui/widgets/genericlistview.py
 src/gui/widgets/statusbar.py

Modified: trunk/src/gui/maindialog.py
==============================================================================
--- trunk/src/gui/maindialog.py	(original)
+++ trunk/src/gui/maindialog.py	Tue Sep 23 20:53:45 2008
@@ -15,6 +15,8 @@
 from gui.widgets.statusbar import Statusbar
 from gui.widgets.viewbill import ViewBill as ViewBill
 from gui.widgets.trayicon import NotifyIcon
+from gui.widgets.chartwidget import ChartWidget
+from gui.widgets.calendarwidget import CalendarWidget
 
 # Import data model modules
 from lib.bill import Bill
@@ -101,10 +103,6 @@
         self._populate_menubar()
 
         self.listbox = gtk.VBox(homogeneous=False, spacing=6)
-        self.listlabel = gtk.Label()
-        self.listlabel.set_markup_with_mnemonic(_("<b>_Bills:</b>"))
-        self.listlabel.set_mnemonic_widget(self.list)
-        self.listlabel.set_alignment(0.00, 0.50)
         # ScrolledWindow
         self.scrolledwindow = gtk.ScrolledWindow()
         self.scrolledwindow.set_shadow_type(gtk.SHADOW_IN)
@@ -112,8 +110,6 @@
                                        gtk.POLICY_AUTOMATIC)
         self.scrolledwindow.add(self.list)
         ## Pack it all up
-        self.listbox.pack_start(self.listlabel,
-           expand=False, fill=True, padding=1)
         self.listbox.pack_start(self.scrolledwindow,
            expand=True, fill=True, padding=2)
 
@@ -122,20 +118,15 @@
 
         # Calendar
         self.calbox = gtk.VBox(homogeneous=False, spacing=1)
-        self.callabel = gtk.Label()
-        self.callabel.set_markup_with_mnemonic(_("<b>_Due Date:</b>"))
-        self.callabel.set_alignment(0.00, 0.50)
-        self.calendar = gtk.Calendar()
-        self.callabel.set_mnemonic_widget(self.calendar)
-        # Format the dueDate field
-        self.calendar.connect("month_changed", self._on_calendar_month_changed)
-        self.calendar.connect("day_selected_double_click", self._on_calendar_double_click)
+        self.calendar = CalendarWidget()
+        self.calendar.connect("date_changed", self._on_calendar_month_changed)
         ## Pack it all up
-        self.calbox.pack_start(self.callabel,
-           expand=False, fill=True, padding=1)
         self.calbox.pack_start(self.calendar,
            expand=True, fill=True, padding=2)
-        self.calendar.mark_day(datetime.datetime.today().day)
+        #self.calendar.mark_day(datetime.datetime.today().day)
+
+        # Chart
+        self.chart = ChartWidget()
 
         # Pack it all up
         self.box.pack_start(self.toolbar,
@@ -144,6 +135,8 @@
             expand=False, fill=True, padding=4)
         self.box.pack_start(self.listbox,
             expand=True, fill=True, padding=4)
+        self.box.pack_start(self.chart,
+            expand=True, fill=True, padding=2)
         self.box.pack_start(self.statusbar,
             expand=False, fill=True, padding=2)
 
@@ -225,11 +218,6 @@
         else:
             self.currentrecord = None
 
-    def _markCalendar(self, records):
-        self.calendar.clear_marks()
-        for rec in records:
-            self.calendar.mark_day(datetime.datetime.fromtimestamp(rec['dueDate']).day)
-
     def _populateTreeView(self, records):
         """ Populates the treeview control with the records passed """
 
@@ -247,8 +235,8 @@
     def reloadTreeView(self, *arg):
         # Update list with updated record
         status = self.gconf_client.get_int(GCONF_GUI_PATH + 'show_paid_bills')
-        month = self.calendar.get_date()[1] + 1
-        year = self.calendar.get_date()[0]
+        month = self.calendar.currentMonth
+        year = self.calendar.currentYear
 
         path = self.list.get_cursor()[0]
         self.list.listStore.clear()
@@ -260,9 +248,12 @@
         # Populate treeview
         self._populateTreeView(records)
         # Mark days in calendar
-        self._markCalendar(records)
+        #self._markCalendar(records)
         # Update status bar
         self._update_statusbar()
+        # populate chart
+        self._populate_chart(status, month, year)
+
         return len(records)
 
     def _formated_row(self, row):
@@ -305,6 +296,14 @@
             _("Not Paid"), _("Mark as not paid"), self.on_btnPaid_clicked)
         self.btnUnpaid.set_is_important(True)
 
+    def _populate_chart(self, status, month, year):
+        chartdata = []
+        records = self.actions.get_monthly_totals(status, month, year)
+        for rec in records:
+            chartdata.append([field for field in rec])
+        #if chartdata:
+        self.chart.plot(chartdata)
+
     def _populate_menubar(self):
         # Create a UIManager instance
         self.uimanager = gtk.UIManager()
@@ -371,8 +370,9 @@
         self.box.pack_start(menubar, expand=False, fill=True, padding=0)
 
     def add_bill(self):
-        selectedDate = scheduler.time_from_calendar(self.calendar.get_date())
-        selectedDate = scheduler.datetime_from_timestamp(selectedDate)
+        #selectedDate = scheduler.time_from_calendar(self.calendar.get_date())
+        #selectedDate = scheduler.datetime_from_timestamp(selectedDate)
+        selectedDate = self.calendar.currentDate
         records = dialogs.add_dialog(parent=self.window, selectedDate=selectedDate)
 
         # Checks if the user did not cancel the action
@@ -574,10 +574,7 @@
     def on_delete_event(self, widget, event, data=None):
         self._quit_application()
 
-    def _on_calendar_double_click(self, widget):
-        self.add_bill()
-
-    def _on_calendar_month_changed(self, widget):
+    def _on_calendar_month_changed(self, widget, args):
         self.reloadTreeView()
 
     def _on_show_toolbar(self, action):

Added: trunk/src/gui/widgets/calendarwidget.py
==============================================================================
--- (empty file)
+++ trunk/src/gui/widgets/calendarwidget.py	Tue Sep 23 20:53:45 2008
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+import gobject
+import datetime
+
+from lib import i18n
+
+MINDATE = 1900
+MAXDATE = 3000
+
+class CalendarWidget(gtk.HBox):
+
+    currentDate = None
+
+    #     #define a custom signal
+    __gsignals__ = dict(date_changed=(gobject.SIGNAL_RUN_FIRST,
+        gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)))
+
+    def __init__(self, currentDate=None):
+        gtk.HBox.__init__(self, False, 4)
+
+        if not currentDate:
+            currentDate = datetime.datetime.today()
+
+        self.currentDate = currentDate
+        self.currentYear = self.currentDate.year
+        self.currentMonth = self.currentDate.month
+
+        self.initialize_interface()
+        self.initialize_values()
+
+    def set_currentDate (self, value):
+        self.currentDate  = value
+        self.currentYear = self.currentDate.year
+        self.currentMonth = self.currentDate.month
+        self.emit_date_changed_signal()
+
+    def emit_date_changed_signal(self):
+        self.emit("date_changed", self.currentDate)
+
+
+    def get_months (self):
+        year = self.currentYear
+        months = [datetime.datetime(year, x, 1).strftime("%B") for x in range(1,13)]
+
+        return months
+
+    def initialize_interface(self):
+
+        label1 = gtk.Label()
+        label1.set_markup_with_mnemonic(_("<b>_Monthly Report:</b>"))
+
+        self.monthSelector = gtk.combo_box_new_text()
+        self.monthSelector.connect("changed", self._on_monthSelector_changed)
+        self.populate_month_selector()
+
+        label2 = gtk.Label()
+        label2.set_markup_with_mnemonic(_("<b>_Year:</b>"))
+
+        placeHolder1 = gtk.Label()
+
+        # gtk.Adjustment(value=0, lower=0, upper=0, step_incr=0, page_incr=0, page_size=0)
+        adj = gtk.Adjustment(self.currentYear, MINDATE, MAXDATE, 1)
+        self.yearSpinner = gtk.SpinButton(adj, 0, 0)
+        self.yearSpinner.set_numeric(True)
+        self.yearSpinner.set_update_policy(gtk.UPDATE_IF_VALID)
+        self.yearSpinner.set_snap_to_ticks(True)
+        self.yearSpinner.connect("changed", self._on_yearSpinner_changed)
+
+        placeHolder2 = gtk.Label()
+
+        self.pack_start(label1, expand=False, fill=False, padding=4)
+        self.pack_start(self.monthSelector, expand=False, fill=False, padding=4)
+        self.pack_start(placeHolder1, expand=True, fill=True, padding=0)
+        self.pack_start(placeHolder2, expand=True, fill=True, padding=0)
+        self.pack_start(label2, expand=False, fill=False, padding=4)
+        self.pack_start(self.yearSpinner, expand=False, fill=False, padding=4)
+
+    def initialize_values(self):
+        self.monthSelector.set_active(self.currentMonth - 1)
+        self.yearSpinner.set_value(self.currentYear)
+
+    def populate_month_selector(self):
+        store = gtk.ListStore(gobject.TYPE_STRING)
+        self.monthSelector.set_model(store)
+
+        for month in self.get_months():
+            store.append([month])
+
+    def _on_yearSpinner_changed(self, spin):
+        self.update_current_date()
+
+    def _on_monthSelector_changed(self, combo):
+        self.update_current_date()
+
+    def update_current_date(self):
+        index = self.monthSelector.get_active()
+
+        month = index + 1
+        year = int(self.yearSpinner.get_value())
+
+        changedDate = datetime.datetime(year, month, 1)
+        self.set_currentDate(changedDate)
+
+gobject.type_register(CalendarWidget)
+
+class BasicWindow:
+
+    # close the window and quit
+    def delete_event(self, widget, event, data=None):
+        gtk.main_quit()
+        return False
+
+    def __init__(self):
+        # Create a new window
+        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+
+        self.window.set_title("Basic Window")
+
+        #self.window.set_size_request(500, 200)
+
+        self.window.connect("delete_event", self.delete_event)
+
+        self.calendar = CalendarWidget()
+        self.calendar.connect("date_changed", self._on_calendar_date_changed)
+        self.window.add(self.calendar)
+
+        self.window.show_all()
+
+    def _on_calendar_date_changed(self, widget, args):
+        print args
+
+def main():
+    gtk.main()
+
+if __name__ == "__main__":
+    example = BasicWindow()
+    main()

Added: trunk/src/gui/widgets/charting.py
==============================================================================
--- (empty file)
+++ trunk/src/gui/widgets/charting.py	Tue Sep 23 20:53:45 2008
@@ -0,0 +1,707 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2008 Toms BauÄis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+
+"""Small charting library that enables you to draw simple bar and
+horizontal bar charts. This library is not intended for scientific graphs.
+More like some visual clues to the user.
+
+Currently chart understands only list of four member lists, in label, value
+fashion. Like:
+    data = [
+        ["Label1", value1, color(optional), background(optional)],
+        ["Label2", value2 color(optional), background(optional)],
+        ["Label3", value3 color(optional), background(optional)],
+    ]
+
+Author: toms baugis gmail com
+Feel free to contribute - more info at Project Hamster web page:
+http://projecthamster.wordpress.com/
+
+Example:
+    # create new chart object
+    chart = Chart(max_bar_width = 40, collapse_whitespace = True) 
+    
+    eventBox = gtk.EventBox() # charts go into eventboxes, or windows
+    place = self.get_widget("totals_by_day") #just some placeholder
+
+    eventBox.add(chart);
+    place.add(eventBox)
+
+    #Let's imagine that we count how many apples we have gathered, by day
+    data = [["Mon", 20], ["Tue", 12], ["Wed", 80],
+            ["Thu", 60], ["Fri", 40], ["Sat", 0], ["Sun", 0]]
+    self.day_chart.plot(data)
+
+"""
+
+import gtk
+import gobject
+import cairo
+import copy
+import math
+
+def set_color(context, color="#f5f5f5"):
+    # Parse out color value
+    if color is None:
+        color = "#2e9455"
+    color = gtk.gdk.color_parse(color)
+    r = float(color.red) / 65536
+    g = float(color.green) / 65536
+    b = float(color.blue) / 65536
+    #r,g,b = color[0] / 255.0, color[1] / 255.0, color[2] / 255.0
+    #r,g,b = color[0], color[1], color[2]
+    context.set_source_rgb(r, g, b)
+
+class Chart(gtk.DrawingArea):
+    """Chart constructor. Optional arguments:
+        orient_vertical = [True|False] - Chart orientation.
+                                         Defaults to vertical
+        max_bar_width = pixels - Maximal width of bar. If not specified,
+                                 bars will stretch to fill whole area
+        values_on_bars = [True|False] - Should bar values displayed on each bar.
+                                        Defaults to False
+        collapse_whitespace = [True|False] - If max_bar_width is set, should
+                                             we still fill the graph area with
+                                             the white stuff and grids and such.
+                                             Defaults to false
+        stretch_grid = [True|False] - Should the grid be of fixed or flex
+                                      size. If set to true, graph will be split
+                                      in 4 parts, which will stretch on resize.
+                                      Defaults to False.
+        animate = [True|False] - Should the bars grow/shrink on redrawing.
+                                 Animation happens only if labels and their
+                                 order match.
+                                 Defaults to True.
+        legend_width = pixels - Legend width in pixels. Will keep you graph
+                                from floating horizontally
+
+        Then there are some defaults, you can override:
+        default_grid_stride - If stretch_grid is set to false, this allows you
+                              to choose granularity of grid. Defaults to 50
+        animation_frames - in how many steps should the animation be done
+        animation_timeout - after how many miliseconds should we draw next frame
+    """
+    def __init__(self, **args):
+        """here is init"""
+        gtk.DrawingArea.__init__(self)
+        self.connect("expose_event", self._expose)
+        self.data, self.prev_data = None, None #start off with an empty hand
+        
+        """now see what we have in args!"""
+        self.orient_vertical = "orient" not in args or args["orient"] == "vertical" # defaults to true
+        
+        self.max_bar_width = None
+        if "max_bar_width" in args: self.max_bar_width = args["max_bar_width"]        
+
+        self.values_on_bars = "values_on_bars" in args and args["values_on_bars"] #defaults to false
+
+        self.collapse_whitespace = "collapse_whitespace" in args and args["collapse_whitespace"] #defaults to false
+        
+        self.stretch_grid = "stretch_grid" in args and args["stretch_grid"] #defaults to false
+
+        self.animate = "animate" not in args or args["animate"] # defaults to true
+        
+        self.legend_width = None
+        if "legend_width" in args: self.legend_width = args["legend_width"]
+        
+        #and some defaults
+        self.default_grid_stride = 50
+        
+        self.animation_frames = 150
+        self.animation_timeout = 20 #in miliseconds
+
+        self.current_frame = self.animation_frames
+        self.freeze_animation = False
+        
+    def _expose(self, widget, event): # expose is when drawing's going on
+        context = widget.window.cairo_create()
+        context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
+        context.clip()
+        
+        if self.orient_vertical:
+            # for simple bars figure, when there is way too much data for bars
+            # and go to lines (yay!)
+            if len(self.data) == 0 or (widget.allocation.width / len(self.data)) > 30: #this is big enough
+                self._bar_chart(context)
+            else:
+                self._area_chart(context)
+
+        else:
+            self._horizontal_bar_chart(context)
+
+        return False
+
+    def plot(self, data):
+        """Draw chart with given data
+            Currently chart understands only list of two member lists, in label, value
+            fashion. Like:
+                data = [
+                    ["Label1", value1],
+                    ["Label2", value2],
+                    ["Label3", value3],
+                ]
+        """
+
+        #check if maybe this chart is animation enabled and we are in middle of animation
+        if self.animate and self.current_frame < self.animation_frames: #something's going on here!
+            self.freeze_animation = True #so we don't catch some nasty race condition
+
+            self.prev_data = copy.copy(self.data)
+            self.new_data, self.max = self._get_factors(data)
+
+            #if so, let's start where we are and move to the new set inst
+            self.current_frame = 0 #start the animation from beginning
+            self.freeze_animation = False
+            return
+
+        if self.animate:
+            """chart animation means gradually moving from previous data set
+               to the new one. prev_data will be the previous set, new_data
+               is copy of the data we have been asked to plot, and data itself
+               will be the moving thing"""
+               
+            self.current_frame = 0
+            self.new_data, self.max = self._get_factors(data)
+
+            if not self.prev_data: #if there is no previous data, set it to zero, so we get a growing animation
+                self.prev_data = copy.deepcopy(self.new_data)
+                for i in range(len(self.prev_data)):
+                    self.prev_data[i]["factor"] = 0
+                    
+            self.data = copy.copy(self.prev_data)
+
+
+            gobject.timeout_add(self.animation_timeout, self._replot)
+        else:
+            self.data, self.max = self._get_factors(data)
+            self._invalidate()
+
+    
+    def _replot(self):
+        """Internal function to do the math, going from previous set to the
+           new one, and redraw graph"""
+        if self.freeze_animation:
+            return True #just wait until they release us!
+
+        if self.window:    #this can get called before expose    
+            # do some sanity checks before thinking about animation
+            # are the source and target of same length?
+            if len(self.prev_data) != len(self.new_data):
+                self.prev_data = copy.copy(self.new_data)
+                self.data = copy.copy(self.new_data)
+                self.current_frame = self.animation_frames #stop animation
+                self._invalidate()
+                return False
+            
+            # have they same labels? (that's important!)
+            for i in range(len(self.prev_data)):
+                if self.prev_data[i]["label"] != self.new_data[i]["label"]:
+                    self.prev_data = copy.copy(self.new_data)
+                    self.data = copy.copy(self.new_data)
+                    self.current_frame = self.animation_frames #stop animation
+                    self._invalidate()
+                    return False
+            
+
+            #ok, now we are good!
+            self.current_frame = self.current_frame + 1
+            
+
+            # using sines for some "swoosh" animation (not really noticeable)
+            # sin(0) = 0; sin(pi/2) = 1
+            pi_factor = math.sin((math.pi / 2.0) * (self.current_frame / float(self.animation_frames)))
+            #pi_factor = math.sqrt(pi_factor) #stretch it a little so the animation can be seen a little better
+            
+            # here we do the magic - go from prev to new
+            # we are fiddling with the calculated sizes instead of raw data - that's much safer
+            bars_below_lim = 0
+            
+            for i in range(len(self.data)):
+                diff_in_factors = self.prev_data[i]["factor"] - self.new_data[i]["factor"]
+                diff_in_values = self.prev_data[i]["value"] - self.new_data[i]["value"]
+                
+                if abs(diff_in_factors * pi_factor) < 0.001:
+                    bars_below_lim += 1
+                
+                
+                self.data[i]["factor"] = self.prev_data[i]["factor"] - (diff_in_factors * pi_factor)
+                self.data[i]["value"] = self.prev_data[i]["value"] - (diff_in_values * pi_factor)
+                
+            if bars_below_lim == len(self.data): #all bars done - stop animation!
+                self.current_frame = self.animation_frames
+                
+
+        if self.current_frame < self.animation_frames:
+            self._invalidate()
+            return True
+        else:
+            self.data = copy.copy(self.new_data)
+            self.prev_data = copy.copy(self.new_data)
+            self._invalidate()
+            return False
+
+    def _invalidate(self):
+        """Force redrawal of chart"""
+        if self.window:    #this can get called before expose    
+            alloc = self.get_allocation()
+            rect = gtk.gdk.Rectangle(alloc.x, alloc.y, alloc.width, alloc.height)
+            self.window.invalidate_rect(rect, True)
+            self.window.process_updates(True)
+            
+    
+    def _get_factors(self, data):
+        """get's max value out of data and calculates each record's factor
+           against it"""
+        max_value = 0
+        self.there_are_floats = False
+        self.there_are_colors = False
+        self.there_are_backgrounds = False
+        
+        for i in range(len(data)):
+            max_value = max(max_value, data[i][1])
+            if isinstance(data[i][1], float):
+                self.there_are_floats = True #we need to know for the scale labels
+                
+            if len(data[i]) > 3 and data[i][2] != None:
+                self.there_are_colors = True
+                
+            if len(data[i]) > 4 and data[i][3] != None:
+                self.there_are_backgrounds = True
+                
+        
+        res = []
+        for i in range(len(data)):
+            if max_value > 0:
+                factor = data[i][1] / float(max_value)
+            else:
+                factor = 0
+            
+            if len(data[i]) > 2:
+                color = data[i][2]
+            else:
+                color = None
+
+            if len(data[i]) > 3:
+                background = data[i][3]
+            else:
+                background = None
+
+            res.append({"label": data[i][0],
+                        "value": data[i][1],
+                        "color": color,
+                        "background": background,
+                        "factor": factor
+                        })
+        
+        return res, max_value
+    
+
+    def _draw_bar(self, context, x, y, w, h, color):
+        """ draws a nice bar"""
+        
+        context.rectangle(x, y, w, h)
+        set_color(context, color)
+        context.fill_preserve()    
+        context.stroke()
+
+        if w > 2 and h > 2:
+            context.rectangle(x + 1, y + 1, w - 2, h - 2)
+            set_color(context, color)
+            context.fill_preserve()    
+            context.stroke()
+
+        if w > 3 and h > 3:
+            context.rectangle(x + 2, y + 2, w - 4, h - 4)
+            set_color(context, color)
+            context.fill_preserve()    
+            context.stroke()
+
+    def _bar_chart(self, context):
+        rect = self.get_allocation()  #x, y, width, height
+        data, records = self.data, len(self.data)
+
+        if not data:
+            return
+
+        # graph box dimensions
+        graph_x = self.legend_width or 50 #give some space to scale labels
+        graph_width = rect.width + rect.x - graph_x
+
+        step = graph_width / float(records)
+        if self.max_bar_width:
+            step = min(step, self.max_bar_width)
+            if self.collapse_whitespace:
+                graph_width = step * records #no need to have that white stuff
+
+        graph_y = rect.y
+        graph_height = graph_y - rect.x + rect.height - 15
+
+        max_size = graph_height - 15
+
+        context.set_line_width(1)
+
+        # TODO put this somewhere else - drawing background and some grid
+        context.rectangle(graph_x - 1, graph_y, graph_width, graph_height)
+        context.set_source_rgb(1, 1, 1)
+        context.fill_preserve()
+        context.stroke()
+
+        #backgrounds
+        if self.there_are_backgrounds:
+            for i in range(records):
+                if data[i]["background"] != None:
+                    set_color(context);
+                    context.rectangle(graph_x + (step * i), 0, step, graph_height)
+                    context.fill_preserve()
+                    context.stroke()
+
+        context.set_line_width(1)
+        context.set_dash ([1, 3]);
+        set_color(context, '#000000')
+
+        # scale lines
+        stride = self.default_grid_stride and self.stretch_grid == False or int(graph_height / 4)
+
+        for y in range(graph_y, graph_y + graph_height, stride):
+            context.move_to(graph_x - 10, y)
+            context.line_to(graph_x + graph_width, y)
+
+        # and borders on both sides, so the graph doesn't fall out
+        context.move_to(graph_x - 1, graph_y)
+        context.line_to(graph_x - 1, graph_y + graph_height + 1)
+        context.move_to(graph_x + graph_width, graph_y)
+        context.line_to(graph_x + graph_width, graph_y + graph_height + 1)
+        
+
+        context.stroke()
+        
+        
+        context.set_dash ([]);
+
+
+        # labels
+        set_color(context, '#000000');
+        for i in range(records):
+            extent = context.text_extents(data[i]["label"]) #x, y, width, height
+            context.move_to(graph_x + (step * i) + (step - extent[2]) / 2.0,
+                            graph_y + graph_height + 13)
+            context.show_text(data[i]["label"])
+
+        # values for max min and average
+        max_label =  self.there_are_floats and "%.1f" % self.max or "%d" % self.max
+        extent = context.text_extents(max_label) #x, y, width, height
+
+        context.move_to(graph_x - extent[2] - 16, rect.y + 10)
+        context.show_text(max_label)
+
+
+        #flip the matrix vertically, so we do not have to think upside-down
+        context.transform(cairo.Matrix(yy = -1, y0 = graph_height))
+
+        context.set_dash ([]);
+        context.set_line_width(0)
+        context.set_antialias(cairo.ANTIALIAS_NONE)
+
+        # bars themselves
+        for i in range(records):
+            color = data[i]["color"]
+            bar_size = graph_height * data[i]["factor"]
+            #on animations we keep labels on top, so we need some extra space there
+            bar_size = bar_size * 0.8 and (self.values_on_bars and self.animate) or bar_size * 0.9
+            bar_size = max(bar_size, 1)
+            
+            gap = step * 0.05
+            bar_x = graph_x + (step * i) + gap
+            bar_width = step - (gap * 2)
+            
+            self._draw_bar(context, bar_x, 0, bar_width, bar_size, color)
+
+        #values
+        #flip the matrix back, so text doesn't come upside down
+        context.transform(cairo.Matrix(yy = -1, y0 = 0))
+        set_color(context, '#000000')
+        context.set_antialias(cairo.ANTIALIAS_DEFAULT)
+
+        if self.values_on_bars:
+            for i in range(records):
+                label = self.there_are_floats and "%.1f" % data[i]["value"] or "%d" % data[i]["value"]
+                extent = context.text_extents(label) #x, y, width, height
+                
+                bar_size = graph_height * data[i]["factor"]
+                
+                bar_size = bar_size * 0.8 and self.animate or bar_size * 0.9
+                    
+                vertical_offset = (step - extent[2]) / 2.0
+                
+                if self.animate or bar_size - vertical_offset < extent[3]:
+                    graph_y = -bar_size - 3
+                else:
+                    graph_y = -bar_size + extent[3] + vertical_offset
+                
+                context.move_to(graph_x + (step * i) + (step - extent[2]) / 2.0,
+                                graph_y)
+                context.show_text(label)
+
+
+    def _ellipsize_text (self, context, text, width):
+        """try to constrain text into pixels by ellipsizing end
+           TODO - check if cairo maybe has ability to ellipsize automatically
+        """
+        extent = context.text_extents(text) #x, y, width, height
+        if extent[2] <= width:
+            return text
+        
+        res = text
+        while res:
+            res = res[:-1]
+            extent = context.text_extents(res + "â") #x, y, width, height
+            if extent[2] <= width:
+                return res + "â"
+        
+        return text # if can't fit - return what we have
+        
+    def _horizontal_bar_chart(self, context):
+        rect = self.get_allocation()  #x, y, width, height
+        data, records = self.data, len(self.data)
+        
+        # ok, start with labels - get the longest now
+        # TODO - figure how to wrap text
+        if self.legend_width:
+            max_extent = self.legend_width
+        else:
+            max_extent = 0
+            for i in range(records):
+                extent = context.text_extents(data[i]["label"]) #x, y, width, height
+                max_extent = max(max_extent, extent[2] + 8)
+        
+        
+        #push graph to the right, so it doesn't overlap, and add little padding aswell
+        graph_x = rect.x + max_extent
+        graph_width = rect.width + rect.x - graph_x
+
+        graph_y = rect.y
+        graph_height = graph_y - rect.x + rect.height
+        
+        
+        step = int(graph_height / float(records)) and records > 0 or 30
+        if self.max_bar_width:
+            step = min(step, self.max_bar_width)
+            if self.collapse_whitespace:
+                graph_height = step * records #resize graph accordingly
+        
+        max_size = graph_width - 15
+
+
+        ellipsize_label = lambda(text): 3
+
+        #now let's put the labels and align them right
+        set_color(context, '#000000');
+        for i in range(records):
+            label = data[i]["label"]
+            if self.legend_width:
+                label = self._ellipsize_text(context, label, max_extent - 8)
+            extent = context.text_extents(label) #x, y, width, height
+            
+            context.move_to(rect.x + max_extent - extent[2] - 8, rect.y + (step * i) + (step + extent[3]) / 2)
+            context.show_text(label)
+        
+        context.stroke()        
+        
+        
+        context.set_line_width(1)
+        
+        # TODO put this somewhere else - drawing background and some grid
+        context.rectangle(graph_x, graph_y, graph_width, graph_height)
+        context.set_source_rgb(1, 1, 1)
+        context.fill_preserve()
+        context.stroke()
+
+
+        context.set_dash ([1, 3]);
+        set_color(context, '#000000')
+
+        # scale lines        
+        grid_stride = self.default_grid_stride and self.stretch_grid == False or (graph_width) / 3.0
+        for x in range(graph_x + grid_stride, graph_x + graph_width - grid_stride, grid_stride):
+            context.move_to(x, graph_y)
+            context.line_to(x, graph_y + graph_height)
+
+        context.move_to(graph_x + graph_width, graph_y)
+        context.line_to(graph_x + graph_width, graph_y + graph_height)
+
+
+        # and borders on both sides, so the graph doesn't fall out
+        context.move_to(graph_x, graph_y)
+        context.line_to(graph_x + graph_width, graph_y)
+        context.move_to(graph_x, graph_y + graph_height)
+        context.line_to(graph_x + graph_width, graph_y + graph_height)
+
+        context.stroke()
+
+        gap = step * 0.05
+        
+        context.set_dash ([]);
+        context.set_line_width(0)
+        context.set_antialias(cairo.ANTIALIAS_NONE)
+
+        # bars themselves
+        for i in range(records):
+            color = data[i]["color"]
+            bar_y = graph_y + (step * i) + gap
+            bar_size = max_size * data[i]["factor"]
+            bar_size = max(bar_size, 1)
+            bar_height = step - (gap * 2)
+
+            self._draw_bar(context, graph_x, bar_y, bar_size, bar_height, color)
+
+
+        #values
+        context.set_antialias(cairo.ANTIALIAS_DEFAULT)
+        set_color(context, '#000000')        
+        if self.values_on_bars:
+            for i in range(records):
+                label = "%.1f" % data[i]["value"] and self.there_are_floats or "%d" % data[i]["value"]
+                extent = context.text_extents(label) #x, y, width, height
+                
+                bar_size = max_size * data[i]["factor"]
+                horizontal_offset = (step + extent[3]) / 2.0 - extent[3]
+                
+                if  bar_size - horizontal_offset < extent[2]:
+                    label_x = graph_x + bar_size + horizontal_offset
+                else:
+                    label_x = graph_x + bar_size - extent[2] - horizontal_offset
+                
+                context.move_to(label_x, graph_y + (step * i) + (step + extent[3]) / 2.0)
+                context.show_text(label)
+
+        else:
+            # values for max min and average
+            context.move_to(graph_x + graph_width + 10, graph_y + 10)
+            max_label = "%.1f" % self.max and self.there_are_floats or "%d" % self.max
+            context.show_text(max_label)
+        
+        
+    def _area_chart(self, context):
+        rect = self.get_allocation()  #x, y, width, height        
+        data, records = self.data, len(self.data)
+
+        if not data:
+            return
+
+        # graph box dimensions
+        graph_x = self.legend_width or 50 #give some space to scale labels
+        graph_width = rect.width + rect.x - graph_x
+        
+        step = graph_width / float(records)
+        graph_y = rect.y
+        graph_height = graph_y - rect.x + rect.height - 15
+        
+        max_size = graph_height - 15
+
+
+
+        context.set_line_width(1)
+        
+        # TODO put this somewhere else - drawing background and some grid
+        context.rectangle(graph_x, graph_y, graph_width, graph_height)
+        context.set_source_rgb(1, 1, 1)
+        context.fill_preserve()
+        context.stroke()
+
+        context.set_line_width(1)
+        context.set_dash ([1, 3]);
+
+
+        #backgrounds
+        if self.there_are_backgrounds:
+            for i in range(records):
+                if data[i]["background"] != None:
+                    set_color(context);
+                    context.rectangle(graph_x + (step * i), 1, step, graph_height - 1)
+                    context.fill_preserve()
+                    context.stroke()
+
+            
+        set_color(context, '#000000')
+        
+        # scale lines
+        stride = self.default_grid_stride and self.stretch_grid == False or int(graph_height / 4)
+            
+        for y in range(graph_y, graph_y + graph_height, stride):
+            context.move_to(graph_x - 10, y)
+            context.line_to(graph_x + graph_width, y)
+
+        # and borders on both sides, so the graph doesn't fall out
+        context.move_to(graph_x - 1, graph_y)
+        context.line_to(graph_x - 1, graph_y + graph_height + 1)
+        context.move_to(graph_x + graph_width, graph_y)
+        context.line_to(graph_x + graph_width, graph_y + graph_height + 1)
+        
+
+        context.stroke()
+        
+        
+        context.set_dash ([]);
+
+        # labels
+        set_color(context, '#000000');
+        for i in range(records):
+            if i % 5 == 0:
+                context.move_to(graph_x + 5 + (step * i), graph_y + graph_height + 13)
+                context.show_text(data[i]["label"])
+
+        # values for max min and average
+        max_label = "%.1f" % self.max and self.there_are_floats or "%d" % self.max
+        extent = context.text_extents(max_label) #x, y, width, height
+
+        context.move_to(graph_x - extent[2] - 16, rect.y + 10)
+        context.show_text(max_label)
+
+
+        context.rectangle(graph_x, graph_y, graph_width, graph_height + 1)
+        context.clip()
+
+        #flip the matrix vertically, so we do not have to think upside-down
+        context.transform(cairo.Matrix(yy = -1, y0 = graph_height))
+
+
+        set_color(context, '#000000');
+        # chart itself
+        for i in range(records):
+            if i == 0:
+                context.move_to(graph_x, -10)
+                context.line_to(graph_x, graph_height * data[i]["factor"] * 0.9)
+                
+            context.line_to(graph_x + (step * i) + (step * 0.5), graph_height * data[i]["factor"] * 0.9)
+
+            if i == records - 1:
+                context.line_to(graph_x  + (step * i) + (step * 0.5),  0)
+                context.line_to(graph_x + graph_width, 0)
+                context.line_to(graph_x + graph_width, -10)
+                
+
+
+        set_color(context)
+        context.fill_preserve()    
+
+        context.set_line_width(3)
+        context.set_line_join (cairo.LINE_JOIN_ROUND);
+        set_color(context, '#000000');
+        context.stroke()

Added: trunk/src/gui/widgets/chartwidget.py
==============================================================================
--- (empty file)
+++ trunk/src/gui/widgets/chartwidget.py	Tue Sep 23 20:53:45 2008
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+from charting import Chart
+
+class ChartWidget(gtk.EventBox):
+    def __init__(self):
+        gtk.EventBox.__init__(self)
+        self.chart = Chart(
+            max_bar_width = 40,
+            animate = False,
+            values_on_bars = True,
+            stretch_grid = True,
+            legend_width = 80)
+
+        self.add(self.chart)
+
+    def plot(self, data):
+        """
+        Populates chart with data passed in.
+        """
+        self.chart.plot(data)
+
+class BasicWindow:
+
+    # close the window and quit
+    def delete_event(self, widget, event, data=None):
+        gtk.main_quit()
+        return False
+
+    def __init__(self):
+        # Create a new window
+        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+
+        self.window.set_title("Basic Window")
+
+        self.window.set_size_request(500, 200)
+
+        self.window.connect("delete_event", self.delete_event)
+
+        #self.activity_chart = Chart(max_bar_width = 20, collapse_whitespace = True)
+        self.chart = ChartWidget()
+
+        place = gtk.Frame()
+        place.add(self.chart)
+        self.window.add(place)
+        self.window.show_all()
+
+        data = [["Rent", 790, '#808080'], ["Gas", 120], ["Food", 280],
+                ["Education", 60], ["Utilities", 140, '#1a3def'], ["Insurance", 0], ["Travel", 0]]
+        self.chart.plot(data)
+
+
+def main():
+    gtk.main()
+
+if __name__ == "__main__":
+    example = BasicWindow()
+    main()

Modified: trunk/src/lib/actions.py
==============================================================================
--- trunk/src/lib/actions.py	(original)
+++ trunk/src/lib/actions.py	Tue Sep 23 20:53:45 2008
@@ -94,9 +94,9 @@
             self.edit_bill(bill)
         return self.dal.delete(CategoriesTable, key)
 
-
+"""
 if not '--standalone' in sys.argv \
    and not sys.argv[0].endswith('billreminderd') \
    and verify_dbus_service(common.DBUS_INTERFACE):
     from lib.dbus_actions import Actions
-
+"""

Modified: trunk/src/lib/dbus_actions.py
==============================================================================
--- trunk/src/lib/dbus_actions.py	(original)
+++ trunk/src/lib/dbus_actions.py	Tue Sep 23 20:53:45 2008
@@ -47,6 +47,27 @@
             record['caId'] = int(record['caId'])
         return record
 
+    def get_monthly_totals(self, status, month, year):
+        # Return a list of categories and totals for the given month
+        # Delimeters for our search
+        firstOfMonth = scheduler.first_of_month(month, year)
+        lastOfMonth = scheduler.last_of_month(month, year)
+
+        # Determine status criteria
+        status = status < 2 and ' = %s' % status or ' in (0,1)'
+
+        stmt = 'select categoryName, sum(amountDue) as amount, color' \
+            ' from br_billstable, br_categoriestable where' \
+            ' paid %s' \
+            ' and dueDate >= ? and dueDate <= ?' \
+            ' and br_categoriestable.Id = br_billstable.catId' \
+            ' GROUP BY catId, color' \
+            ' ORDER BY dueDate ASC' % status
+        params = [firstOfMonth, lastOfMonth]
+        records = self.executeSql(stmt, params)
+
+        return records
+
     def get_monthly_bills(self, status, month, year):
         # Delimeters for our search
         firstOfMonth = scheduler.first_of_month(month, year)



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]