billreminder r643 - in trunk: . po src/gui src/gui/widgets src/lib
- From: ogmaciel svn gnome org
- To: svn-commits-list gnome org
- Subject: billreminder r643 - in trunk: . po src/gui src/gui/widgets src/lib
- Date: Tue, 23 Sep 2008 20:53:45 +0000 (UTC)
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]