hamster-applet r712 - in trunk: data hamster
- From: tbaugis svn gnome org
- To: svn-commits-list gnome org
- Subject: hamster-applet r712 - in trunk: data hamster
- Date: Fri, 13 Feb 2009 20:47:03 +0000 (UTC)
Author: tbaugis
Date: Fri Feb 13 20:47:03 2009
New Revision: 712
URL: http://svn.gnome.org/viewvc/hamster-applet?rev=712&view=rev
Log:
new, cleaner graphs in overview window.
should update docstrings in charting py
fixes bug 532722
Modified:
trunk/data/stats.glade
trunk/hamster/charting.py
trunk/hamster/db.py
trunk/hamster/stats.py
trunk/hamster/storage.py
Modified: trunk/data/stats.glade
==============================================================================
--- trunk/data/stats.glade (original)
+++ trunk/data/stats.glade Fri Feb 13 20:47:03 2009
@@ -1,12 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
-<!--Generated with glade3 3.4.5 on Thu Feb 5 14:41:32 2009 -->
+<!--Generated with glade3 3.4.5 on Fri Feb 13 20:35:30 2009 -->
<glade-interface>
<widget class="GtkWindow" id="stats_window">
- <property name="height_request">500</property>
+ <property name="width_request">600</property>
+ <property name="height_request">400</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="title" translatable="yes">Overview - Hamster</property>
<property name="window_position">GTK_WIN_POS_CENTER</property>
+ <property name="default_width">800</property>
<property name="default_height">600</property>
<property name="icon_name">hamster-applet</property>
<signal name="key_press_event" handler="on_window_key_pressed"/>
@@ -204,7 +206,6 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="headers_clickable">True</property>
<property name="enable_search">False</property>
<signal name="key_press_event" handler="on_key_pressed"/>
<signal name="row_activated" handler="on_facts_row_activated"/>
@@ -242,110 +243,133 @@
</child>
<child>
<widget class="GtkVBox" id="vbox2">
- <property name="width_request">390</property>
<property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="homogeneous">True</property>
<child>
- <widget class="GtkFrame" id="frame1">
+ <widget class="GtkScrolledWindow" id="scrolledwindow2">
<property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="shadow_type">GTK_SHADOW_IN</property>
<child>
- <widget class="GtkAlignment" id="totals_by_day">
+ <widget class="GtkEventBox" id="graph_frame">
<property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="border_width">8</property>
- <property name="left_padding">12</property>
<child>
- <placeholder/>
+ <widget class="GtkVBox" id="frame66">
+ <property name="width_request">390</property>
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="border_width">4</property>
+ <property name="spacing">24</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <widget class="GtkFrame" id="fram1">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <child>
+ <widget class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="border_width">9</property>
+ <property name="spacing">36</property>
+ <child>
+ <widget class="GtkEventBox" id="totals_by_category">
+ <property name="width_request">50</property>
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkEventBox" id="totals_by_day">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="dayview_caption">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Week</property>
+ <property name="use_markup">True</property>
+ </widget>
+ <packing>
+ <property name="type">label_item</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkFrame" id="frame39">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <child>
+ <widget class="GtkEventBox" id="totals_by_activity">
+ <property name="visible">True</property>
+ <property name="border_width">12</property>
+ <child>
+ <placeholder/>
+ </child>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="label" translatable="yes">Activity</property>
+ <property name="use_markup">True</property>
+ </widget>
+ <packing>
+ <property name="type">label_item</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </widget>
</child>
</widget>
</child>
- <child>
- <widget class="GtkLabel" id="dayview_caption">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label" translatable="yes"><b>Week</b></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="type">label_item</property>
- </packing>
- </child>
</widget>
</child>
<child>
- <widget class="GtkFrame" id="frame2">
+ <widget class="GtkAlignment" id="alignment3">
<property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">GTK_SHADOW_NONE</property>
- <child>
- <widget class="GtkAlignment" id="totals_by_category">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="border_width">8</property>
- <property name="left_padding">12</property>
- <child>
- <placeholder/>
- </child>
- </widget>
- </child>
+ <property name="xalign">1</property>
+ <property name="xscale">0</property>
+ <property name="top_padding">18</property>
<child>
- <widget class="GtkLabel" id="label2">
+ <widget class="GtkLabel" id="totals">
<property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label" translatable="yes"><b>Category</b></property>
- <property name="use_markup">True</property>
+ <property name="label" translatable="yes">Something: 20:00, Food: 12:13</property>
</widget>
- <packing>
- <property name="type">label_item</property>
- </packing>
</child>
</widget>
<packing>
+ <property name="expand">False</property>
<property name="position">1</property>
</packing>
</child>
- <child>
- <widget class="GtkFrame" id="frame3">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label_xalign">0</property>
- <property name="shadow_type">GTK_SHADOW_NONE</property>
- <child>
- <widget class="GtkAlignment" id="totals_by_activity">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="border_width">8</property>
- <property name="left_padding">12</property>
- <child>
- <placeholder/>
- </child>
- </widget>
- </child>
- <child>
- <widget class="GtkLabel" id="label3">
- <property name="visible">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="label" translatable="yes"><b>Activity</b></property>
- <property name="use_markup">True</property>
- </widget>
- <packing>
- <property name="type">label_item</property>
- </packing>
- </child>
- </widget>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
</widget>
<packing>
- <property name="resize">False</property>
- <property name="shrink">False</property>
+ <property name="resize">True</property>
+ <property name="shrink">True</property>
</packing>
</child>
</widget>
Modified: trunk/hamster/charting.py
==============================================================================
--- trunk/hamster/charting.py (original)
+++ trunk/hamster/charting.py Fri Feb 13 20:47:03 2009
@@ -26,8 +26,8 @@
fashion. Like:
data = [
["Label1", value1, color(optional), background(optional)],
- ["Label2", value2 color(optional), background(optional)],
- ["Label3", value3 color(optional), background(optional)],
+ ["Label2", value2, color(optional), background(optional)],
+ ["Label3", value3, color(optional), background(optional)],
]
Author: toms baugis gmail com
@@ -36,7 +36,7 @@
Example:
# create new chart object
- chart = Chart(max_bar_width = 40, collapse_whitespace = True)
+ chart = Chart(max_bar_width = 40)
eventBox = gtk.EventBox() # charts go into eventboxes, or windows
place = self.get_widget("totals_by_day") #just some placeholder
@@ -72,8 +72,11 @@
color_count = len(light)
-def set_color(context, color):
- r,g,b = color[0] / 255.0, color[1] / 255.0, color[2] / 255.0
+def set_color(context, color, g = None, b = None):
+ if g and b:
+ r,g,b = color / 255.0, g / 255.0, b / 255.0
+ else:
+ r,g,b = color[0] / 255.0, color[1] / 255.0, color[2] / 255.0
context.set_source_rgb(r, g, b)
def set_color_gdk(context, color):
@@ -88,10 +91,6 @@
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.
@@ -113,7 +112,7 @@
"""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
+ self.data, self.prev_factors = 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
@@ -123,442 +122,365 @@
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.show_scale = "show_scale" not in args or args["show_scale"] # defaults to true
+
+ self.background = args.get("background", None)
+ self.chart_background = args.get("chart_background", None)
+ self.bar_base_color = args.get("bar_base_color", None)
+
+ self.bars_beveled = "bars_beveled" not in args or args["bars_beveled"] # defaults to true
+
- self.legend_width = None
- if "legend_width" in args: self.legend_width = args["legend_width"]
+ self.legend_width = args.get("legend_width", 0)
+ self.show_total = "show_total" in args and args["show_total"] #defaults to false
+
+ self.labels_at_end = "labels_at_end" in args and args["labels_at_end"] #defaults to false
+
#and some defaults
self.default_grid_stride = 50
- self.animation_frames = 150
- self.animation_timeout = 20 #in miliseconds
+ self.animation_frames = 100
+ self.animation_timeout = 15 #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()
+ self.value_format = args.get("value_format", "%s")
+
+ self.show_series = "show_series" not in args or args["show_series"] # defaults to true
+
+ if "legend_keys" in args: self.legend_keys = args["legend_keys"]
+
+
+ self.grid_stride = args.get("grid_stride", None)
+
+
+
+ self.keys, self.series_keys = None, None
+ self.factors = None
+ self.row_max = 0
+ self.current_max = 0
+
+ def _expose(self, widget, event):
+ """expose is when drawing's going on, like on _invalidate"""
+
+ self.context = widget.window.cairo_create()
+ self.context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
+ self.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)
-
-
+ self._multibar_chart(self.context)
else:
- self._horizontal_bar_chart(context)
+ self._horizontal_bar_chart()
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],
- ]
- """
+
+ def get_row_max(self, values):
+ res = None
+ for row in values:
+ if type(row) in [int, float]:
+ res = max(res, row)
+ else:
+ res = max(res, sum(row))
+ return res
+
+ def calculate_factors(self, values, max_value):
+ factors = []
+ max_value = float(max_value) #factors need precision
+ if not values: return None
+
+ if not max_value:
+ if type(values[0]) in [int, float]:
+ return [0] * len(values)
+ else:
+ return [[0] * len(values[0])] * len(values)
+
+
+ for row in values:
+ if type(row) in [int, float]:
+ factors.append(row / max_value)
+ else:
+ factors.append([col / max_value for col in row])
+
+ return factors
+
+
+ def plot2(self, keys, data, series_keys = None):
+ """Draw chart with given data"""
+
+ self.data = data
+
+ self.prev_keys, self.prev_series_keys = copy.copy(self.keys), copy.copy(self.series_keys)
+
+ self.keys, self.series_keys = keys, series_keys
+
+ if not self.data:
+ self._invalidate()
+ return
+
+
+ self.prev_factors = copy.copy(self.factors)
+ self.prev_row_max = self.row_max
+
+ self.row_max = self.get_row_max(data)
+ self.new_factors = self.calculate_factors(data, self.row_max)
#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
+ """chart animation gradually moves from current data set
+ to the new one. prev_factors will be the previous set, new_factors
+ is what we have been asked to plot, and factors 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
+ #if there is no previous data, set it to zero, so we get a growing animation
+ if not self.prev_factors:
+ if series_keys:
+ #watch out of mutable arrays
+ self.factors = self.prev_factors = \
+ [[0] * len(series_keys) for x in range(len(keys))]
+ else:
+ self.factors = self.prev_factors = [0] * len(keys)
+
+ self.prev_keys, self.prev_series_keys = self.keys, self.series_keys
- 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.factors = self.new_factors
self._invalidate()
-
+
+ def _smoothstep(self, v, start, end):
+ smooth = 1 - (1 - v) * (1 - v)
+ return (end * smooth) + (start * (1-smooth))
+
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
-
+ if self.current_frame == self.animation_frames:
+ return False;
- res = []
- for i in range(len(data)):
- factor = 0
- if max_value > 0:
- factor = data[i][1] / float(max_value)
-
- color = None
- if len(data[i]) > 2:
- color = data[i][2]
-
- background = None
- if len(data[i]) > 3:
- background = data[i][3]
-
- 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, dark[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, light[color])
- context.fill_preserve()
- context.stroke()
+ #this can get called before expose
+ if not self.window:
+ self._invalidate()
+ return False
- if w > 3 and h > 3:
- context.rectangle(x + 2, y + 2, w - 4, h - 4)
- set_color(context, medium[color])
- context.fill_preserve()
- context.stroke()
+ #ok, now we are good!
+ self.current_frame = self.current_frame + 1
+ frame = self.current_frame / float(self.animation_frames)
-
- def _bar_chart(self, context):
- rect = self.get_allocation() #x, y, width, height
- data, records = self.data, len(self.data)
+ self.current_max = self._smoothstep(frame, self.prev_row_max, self.row_max)
- if not data:
- return
+ # do some sanity checks before thinking about animation
+ # are the source and target of same length?
+ similar_keys = False
+ for i in range(min(len(self.keys), len(self.prev_keys))):
+ if self.keys[i] == self.prev_keys[i]:
+ similar_keys = True
+ break
- # graph box dimensions
- graph_x = self.legend_width or 50 #give some space to scale labels
- graph_width = rect.width + rect.x - graph_x
+ if not similar_keys:
+ self.factors = self.new_factors
+ self._invalidate()
+ return True
- 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 = rect.height - 15
+ keys_len = len(self.keys)
+ prev_keys_len = len(self.prev_keys)
- max_size = graph_height - 15
+ if self.series_keys:
+ ser_keys_len = len(self.series_keys)
+ prev_ser_keys_len = len(self.prev_series_keys)
+
+ for i in range(len(self.keys)):
+ if i < keys_len and i < prev_keys_len \
+ and self.keys[i] == self.prev_keys[i]:
+
+ if self.series_keys:
+ for j in range(len(self.series_keys)):
+ if j < ser_keys_len and j < prev_ser_keys_len \
+ and self.series_keys[j] == self.prev_series_keys[j]:
+ self.factors[i][j] = self._smoothstep(frame, self.prev_factors[i][j], self.new_factors[i][j])
+ elif j>= len(self.factors[i]):
+ self.factors.append(self.new_factors[i][j])
+ else:
+ self.factors[i][j] = self.new_factors[i][j]
+ else:
+ self.factors[i] = self._smoothstep(frame, self.prev_factors[i], self.new_factors[i])
+ elif i >= len(self.factors):
+ self.factors.append(self.new_factors[i])
+ else:
+ self.factors[i] = self.new_factors[i]
+ self._invalidate()
+ return self.current_frame < self.animation_frames #return if there is still work to do
- 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, light[data[i]["background"]]);
- context.rectangle(graph_x + (step * i), 0, step, graph_height)
- context.fill_preserve()
- context.stroke()
+ 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)
- context.set_line_width(1)
- context.set_dash ([1, 3]);
- set_color(context, dark[8])
-
- # scale lines
- stride = int(graph_height / 4)
- if self.stretch_grid == False:
- stride = self.default_grid_stride
-
- 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()
-
+ def _draw_bar(self, x, y, w, h, color = None):
+ """ draws a nice bar"""
+ context = self.context
- context.set_dash ([]);
-
-
- # scale labels
- set_color_gdk(context, self.style.fg[gtk.STATE_NORMAL]);
- 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 = "%d" % self.max
- if self.there_are_floats:
- max_label = "%.1f" % 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)
+ base_color = color or self.bar_base_color or (220, 220, 220)
-
- #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"] or 3
-
- bar_size = graph_height * data[i]["factor"]
- #on animations we keep labels on top, so we need some extra space there
- if self.values_on_bars and self.animate:
- bar_size = bar_size * 0.8
- else:
- bar_size = 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)
+ if self.bars_beveled:
+ context.rectangle(x, y, w, h)
+ set_color(context, *[b - 30 for b in base_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, *[b + 20 for b in base_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, *base_color)
+ context.fill_preserve()
+ context.stroke()
+ else:
+ context.rectangle(x, y, w, h)
+ set_color(context, *base_color)
+ context.fill_preserve()
+ context.stroke()
- 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, dark[8])
- context.set_antialias(cairo.ANTIALIAS_DEFAULT)
-
- if self.values_on_bars:
- for i in range(records):
- if self.there_are_floats:
- label = "%.1f" % data[i]["value"]
- else:
- label = "%d" % data[i]["value"]
- extent = context.text_extents(label) #x, y, width, height
-
- bar_size = graph_height * data[i]["factor"]
-
- if self.animate:
- bar_size = bar_size * 0.8
- else:
- bar_size = 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):
+ def _ellipsize_text (self, 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
+ extent = self.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
+ extent = self.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 _longest_label(self, labels):
+ max_extent = 0
+ for label in labels:
+ extent = self.context.text_extents(label) #x, y, width, height
+ max_extent = max(max_extent, extent[2] + 8)
+
+ return max_extent
+
+
+ def _horizontal_bar_chart(self):
+ rect = self.get_allocation() #x, y, width, height of the whole drawing area
- def _horizontal_bar_chart(self, context):
- rect = self.get_allocation() #x, y, width, height
- data, records = self.data, len(self.data)
+ rowcount, keys = len(self.keys), self.keys
- # ok, start with labels - get the longest now
+ context = self.context
+
+ # get the longest label
# 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)
+ longest_label = max(self.legend_width, self._longest_label(keys))
+ if self.background:
+ # TODO put this somewhere else - drawing background and some grid
+ context.rectangle(rect.x, rect.y, rect.width, rect.height)
+
+ context.set_source_rgb(*self.background)
+ context.fill_preserve()
+ context.stroke()
+
#push graph to the right, so it doesn't overlap, and add little padding aswell
- graph_x = rect.x + max_extent
+ graph_x = rect.x + longest_label
graph_width = rect.width + rect.x - graph_x
+ graph_y, graph_height = rect.y, rect.height
- graph_y = rect.y
- graph_height = rect.height
-
+
+ if self.chart_background:
+ # TODO put this somewhere else - drawing background and some grid
+ context.rectangle(graph_x, graph_y, graph_width, graph_height)
+ context.set_source_rgb(*self.chart_background)
+ context.fill_preserve()
+ context.stroke()
- if records > 0:
- step = int(graph_height / float(records))
- else:
- step = 30
+
+ """
+ # stripes for the case i decided that they are not annoying
+ for i in range(0, round(self.current_max), 10):
+ x = graph_x + (graph_width * (i / float(self.current_max)))
+ w = (graph_width * (5 / float(self.current_max)))
+
+ context.set_source_rgb(0.90, 0.90, 0.90)
+ context.rectangle(x + w, graph_y, w, graph_height)
+ context.fill_preserve()
+ context.stroke()
- 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
+ context.set_source_rgb(0.70, 0.70, 0.70)
+ context.move_to(x, graph_y + graph_height - 2)
+
+ context.show_text(str(i))
+ """
+
+ if not self.data: #if we have nothing, let's go home
+ return
+
+ bar_width = int(graph_height / float(rowcount))
+ if self.max_bar_width:
+ bar_width = min(bar_width, self.max_bar_width)
- ellipsize_label = lambda(text): 3
+
+ max_bar_size = graph_width - 15
+ gap = bar_width * 0.05
- # now let's put scale labels and align them right
+ # keys
set_color_gdk(context, self.style.fg[gtk.STATE_NORMAL]);
- for i in range(records):
- label = data[i]["label"]
- if self.legend_width:
- label = self._ellipsize_text(context, label, max_extent - 8)
+ for i in range(rowcount):
+ label = keys[i]
+
+ if self.legend_width > 0:
+ label = self._ellipsize_text(label, longest_label - 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.move_to(rect.x + longest_label - extent[2] - 8, rect.y + (bar_width * i) + (bar_width + extent[3]) / 2)
context.show_text(label)
context.stroke()
@@ -566,203 +488,255 @@
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, dark[8])
-
- # scale lines
- if self.stretch_grid == False:
- grid_stride = self.default_grid_stride
- else:
- grid_stride = int(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"] or 3
- 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)
+ # bars themselves
+ for i in range(rowcount):
+ bar_start = 0
+ base_color = self.bar_base_color or (220, 220, 220)
+
+ gap = bar_width * 0.05
+
+ bar_y = graph_y + (bar_width * i) + gap
+
+ for j in range(len(self.factors[i])):
+ factor = self.factors[i][j]
+ if factor > 0:
+ bar_size = max_bar_size * factor
+ bar_height = bar_width - (gap * 2)
+
+ self._draw_bar(graph_x,
+ bar_y,
+ bar_size,
+ bar_height,
+ [col - (j * 22) for col in base_color])
+
+ bar_start += bar_size
#values
context.set_antialias(cairo.ANTIALIAS_DEFAULT)
set_color(context, dark[8])
if self.values_on_bars:
- for i in range(records):
- if self.there_are_floats:
- label = "%.1f" % data[i]["value"]
- else:
- label = "%d" % data[i]["value"]
+ for i in range(rowcount):
+ label = self.value_format % sum(self.data[i])
+ factor = sum(self.factors[i])
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]
+ bar_size = max_bar_size * factor
+ horizontal_offset = (bar_width + 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.move_to(label_x, graph_y + (bar_width * i) + (bar_width + 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)
- if self.there_are_floats:
- max_label = "%.1f" % self.max
- else:
- max_label = "%d" % self.max
-
+ # show max value
+ context.move_to(graph_x + graph_width - 30, graph_y + 10)
+ max_label = self.value_format % self.current_max
context.show_text(max_label)
-
-
- def _area_chart(self, context):
+
+ def _multibar_chart(self, context):
rect = self.get_allocation() #x, y, width, height
- data, records = self.data, len(self.data)
- if not data:
- return
+ rowcount, keys = len(self.keys), self.keys
# graph box dimensions
- graph_x = self.legend_width or 50 #give some space to scale labels
- graph_width = rect.width + rect.x - graph_x
+ if self.show_scale:
+ self.legend_width = max(self.legend_width, 20)
- step = graph_width / float(records)
+ if self.series_keys and self.labels_at_end:
+ graph_x = 0
+ graph_width = rect.width - max(self.legend_width, self._longest_label(self.series_keys))
+ else:
+ graph_x = self.legend_width #give some space to scale labels
+ graph_width = rect.width + rect.x - graph_x - 10
+
graph_y = rect.y
graph_height = 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()
+ if self.background:
+ # TODO put this somewhere else - drawing background and some grid
+ context.rectangle(rect.x, rect.y, rect.width, rect.height)
+
+ context.set_source_rgb(*self.background)
+ context.fill_preserve()
+ context.stroke()
+
- context.set_line_width(1)
- context.set_dash ([1, 3]);
+ if self.chart_background:
+ # 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(*self.chart_background)
+ context.fill_preserve()
+ context.stroke()
+ bar_width = min(graph_width / float(rowcount), self.max_bar_width)
- #backgrounds
- if self.there_are_backgrounds:
- for i in range(records):
- if data[i]["background"] != None:
- set_color(context, light[data[i]["background"]]);
- context.rectangle(graph_x + (step * i), 1, step, graph_height - 1)
- context.fill_preserve()
- context.stroke()
+ # keys
+ prev_end = None
+ set_color(context, dark[8]);
+ for i in range(len(keys)):
+ extent = context.text_extents(keys[i]) #x, y, width, height
+ intended_x = graph_x + (bar_width * i) + (bar_width - extent[2]) / 2.0
- set_color(context, dark[8])
-
- # scale lines
- if self.stretch_grid == False:
- stride = self.default_grid_stride
- else:
- stride = int(graph_height / 4)
+ if not prev_end or intended_x > prev_end:
+ context.move_to(intended_x, graph_y + graph_height + 13)
+ context.show_text(keys[i])
- 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)
+ prev_end = intended_x + extent[2] + 10
+
- # 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)
-
+ # maximal
+ if self.show_total:
+ max_label = "%d" % self.row_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.stroke()
-
-
context.set_dash ([]);
+ context.set_line_width(0)
+ context.set_antialias(cairo.ANTIALIAS_NONE)
+
+ max_bar_size = graph_height
+ #make sure bars don't hit the ceiling
+ if self.animate:
+ max_bar_size = graph_height - 10
- # scale labels
- set_color_gdk(context, self.style.fg[gtk.STATE_NORMAL]);
- 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
- if self.there_are_floats:
- max_label = "%.1f" % self.max
- else:
- max_label = "%d" % self.max
+
+ # bars themselves
+ for i in range(rowcount):
+ color = 3
+
+ bar_start = 0
- extent = context.text_extents(max_label) #x, y, width, height
+ base_color = self.bar_base_color or (220, 220, 220)
- context.move_to(graph_x - extent[2] - 16, rect.y + 10)
- context.show_text(max_label)
+ gap = bar_width * 0.05
+ bar_x = graph_x + (bar_width * i) + gap
+ for j in range(len(self.factors[i])):
+ factor = self.factors[i][j]
+ if factor > 0:
+ bar_size = max_bar_size * factor
+
+ self._draw_bar(bar_x+1,
+ bar_start,
+ bar_width-2 - (gap * 2),
+ bar_size,
+ [col - (j * 22) for col in base_color])
+
+ bar_start += bar_size
+ color +=1
+ if color > 2:
+ color = 0
- 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))
+ #flip the matrix back, so text doesn't come upside down
+ context.transform(cairo.Matrix(yy = -1, y0 = 0))
+ set_color(context, dark[8])
+ context.set_line_width(1)
+ label_height = 10
- set_color(context, dark[4]);
- # 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)
-
+ #white grid and scale values
+ if self.grid_stride and self.row_max:
+ # if grid stride is less than 1 then we consider it to be percentage
+ if self.grid_stride < 1:
+ grid_stride = int(self.row_max * self.grid_stride)
+ else:
+ grid_stride = int(self.grid_stride)
+
+ context.set_line_width(1)
+ for i in range(grid_stride, int(self.row_max), grid_stride):
+ y = - max_bar_size * (i / self.row_max)
+ label = str(i)
+ extent = context.text_extents(label) #x, y, width, height
+ context.move_to(rect.x + self.legend_width - extent[2] - 2, y + label_height / 2)
+ set_color(context, medium[8])
+ context.show_text(label)
+ context.stroke()
- set_color(context, light[4])
- context.fill_preserve()
+ set_color(context, (255,255,255))
+ context.move_to(graph_x, y)
+ context.line_to(graph_x + graph_width, y)
+ context.stroke()
+
- context.set_line_width(3)
- context.set_line_join (cairo.LINE_JOIN_ROUND);
- set_color(context, dark[4]);
- context.stroke()
-
+ context.set_antialias(cairo.ANTIALIAS_DEFAULT)
+
+
+ #series keys
+ if self.show_series:
+ #put series keys
+ longest_label = max(self.legend_width, self._longest_label(self.series_keys))
+ set_color(context, dark[8]);
+
+ y = 0
+ label_y = None
+
+ # if labels are at end, then we need show them for the last bar!
+ if self.labels_at_end:
+ factors = self.factors[0]
+ else:
+ factors = self.factors[-1]
+
+ for j in range(len(factors)):
+ factor = factors[j]
+ bar_size = factor * max_bar_size
+
+ if round(bar_size) > 0:
+ label = "%s" % self.series_keys[j]
+
+
+ if self.legend_width:
+ label = self._ellipsize_text(label, longest_label - 8)
+
+ extent = context.text_extents(label) #x, y, width, height
+
+ y -= bar_size
+ intended_position = round(y + (bar_size + extent[3]) / 2)
+
+ if label_y:
+ label_y = min(intended_position, label_y - label_height * 1.5)
+ else:
+ label_y = intended_position
+
+ if self.labels_at_end:
+ label_x = graph_x + graph_width
+ line_x1 = graph_x + graph_width - 1
+ line_x2 = graph_x + graph_width - 6
+ else:
+ label_x = rect.x + longest_label - extent[2] - 8
+ line_x1 = rect.x + longest_label - 4
+ line_x2 = rect.x + longest_label + 1
+ context.move_to(label_x, label_y)
+ context.show_text(label)
+
+ if label_y != intended_position:
+ context.move_to(line_x1, label_y - extent[3] / 2)
+ context.line_to(line_x2, round(y + bar_size / 2))
+
+
+ context.stroke()
+
Modified: trunk/hamster/db.py
==============================================================================
--- trunk/hamster/db.py (original)
+++ trunk/hamster/db.py Fri Feb 13 20:47:03 2009
@@ -331,7 +331,7 @@
a.end_time AS end_time,
a.description as description,
b.name AS name, b.id as activity_id,
- coalesce(c.name, ?) as category, c.id as category_id
+ coalesce(c.name, ?) as category, coalesce(c.id, -1) as category_id
FROM facts a
LEFT JOIN activities b ON a.activity_id = b.id
LEFT JOIN categories c on b.category_id = c.id
@@ -342,6 +342,33 @@
return self.fetchall(query, (_("Unsorted"), date, end_date))
+ def __get_popular_categories(self):
+ """returns categories used in the specified interval"""
+ query = """
+ SELECT coalesce(c.name, ?) as category, count(a.id) as popularity
+ FROM facts a
+ LEFT JOIN activities b on a.activity_id = b.id
+ LEFT JOIN categories c on c.id = b.category_id
+ GROUP BY b.category_id
+ ORDER BY popularity desc
+ """
+ return self.fetchall(query, (_("Unsorted"), ))
+
+ def __get_interval_activity_ids(self, date, end_date = None):
+ """returns activities used in the specified interval"""
+ query = """
+ SELECT a.name, coalesce(b.name, ?) as category_name
+ FROM activities a
+ LEFT JOIN categories b on b.id = a.category_id
+ WHERE a.id in (SELECT activity_id from facts
+ WHERE date(start_time) >= ?
+ AND date(start_time) <= ?)
+ """
+ end_date = end_date or date
+
+ return self.fetchall(query, (_("Unsorted"), date, end_date))
+
+
def __remove_fact(self, fact_id):
query = """
DELETE FROM facts
Modified: trunk/hamster/stats.py
==============================================================================
--- trunk/hamster/stats.py (original)
+++ trunk/hamster/stats.py Fri Feb 13 20:47:03 2009
@@ -26,12 +26,12 @@
import pango
from hamster import dispatcher, storage, SHARED_DATA_DIR, stuff
-from hamster.charting import Chart
+from hamster import charting
+
from hamster.add_custom_fact import CustomFactController
import datetime as dt
import calendar
-import gobject
import time
class StatsViewer:
@@ -58,46 +58,66 @@
timeColumn.set_cell_data_func(timeCell, self.duration_painter)
self.fact_tree.append_column(timeColumn)
- self.fact_store = gtk.TreeStore(int, str, str, str, str) #id, caption, duration, date (invisible), description
+ #id, caption, duration, date (invisible), description
+ self.fact_store = gtk.TreeStore(int, str, str, str, str)
self.fact_tree.set_model(self.fact_store)
+
+
+ graph_frame = self.get_widget("graph_frame")
+
+ background = (0.975,0.975,0.975)
+
+ graph_frame.modify_bg(gtk.STATE_NORMAL,
+ gtk.gdk.Color(*[b*65536.0 for b in background]))
+
+
+
x_offset = 80 # let's nicely align all graphs
- self.day_chart = Chart(max_bar_width = 40,
- collapse_whitespace = True,
- legend_width = x_offset)
- eventBox = gtk.EventBox()
- place = self.get_widget("totals_by_day")
- eventBox.add(self.day_chart);
- place.add(eventBox)
-
- self.category_chart = Chart(orient = "horizontal",
- max_bar_width = 30,
- animate=False,
- values_on_bars = True,
- stretch_grid = True,
- legend_width = x_offset)
- eventBox = gtk.EventBox()
- place = self.get_widget("totals_by_category")
- eventBox.add(self.category_chart);
- place.add(eventBox)
-
- self.activity_chart = Chart(orient = "horizontal",
- max_bar_width = 25,
- animate = False,
- values_on_bars = True,
- stretch_grid = True,
- legend_width = x_offset)
- eventBox = gtk.EventBox()
- place = self.get_widget("totals_by_activity")
- eventBox.add(self.activity_chart);
- place.add(eventBox)
+ self.category_chart = charting.Chart(background = background,
+ bar_base_color = (238,221,221),
+ bars_beveled = False,
+ legend_width = x_offset,
+ max_bar_width = 35
+ )
+
+ category_box = self.get_widget("totals_by_category")
+ category_box.add(self.category_chart)
+ category_box.set_size_request(120, -1)
+
+
+ self.day_chart = charting.Chart(background = background,
+ bar_base_color = (220, 220, 220),
+ bars_beveled = False,
+ show_series = False,
+ max_bar_width = 35,
+ grid_stride = 4)
+
+ self.get_widget("totals_by_day").add(self.day_chart)
+
+
+
+ self.activity_chart = charting.Chart(orient = "horizontal",
+ max_bar_width = 25,
+ values_on_bars = True,
+ stretch_grid = True,
+ legend_width = x_offset,
+ value_format = "%.1f",
+ background = background,
+ bars_beveled = False,
+ animate = False)
+ self.get_widget("totals_by_activity").add(self.activity_chart);
+
self.view_date = dt.date.today()
- self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1) #set to monday
+ #set to monday
+ self.start_date = self.view_date - \
+ dt.timedelta(self.view_date.weekday() + 1)
# look if we need to start on sunday or monday
- self.start_date = self.start_date + dt.timedelta(self.locale_first_weekday())
+ self.start_date = self.start_date + \
+ dt.timedelta(self.locale_first_weekday())
self.end_date = self.start_date + dt.timedelta(6)
@@ -117,7 +137,8 @@
dispatcher.add_handler('day_updated', self.after_fact_update)
selection = self.fact_tree.get_selection()
- selection.connect('changed', self.on_fact_selection_changed, self.fact_store)
+ selection.connect('changed', self.on_fact_selection_changed,
+ self.fact_store)
self.glade.signal_autoconnect(self)
self.fact_tree.grab_focus()
@@ -171,7 +192,7 @@
text = '<span weight="heavy" rise="-20000">%s</span>' % text
cell.set_property('markup', text)
- def get_facts(self):
+ def get_facts(self, facts):
self.fact_store.clear()
totals = {}
@@ -179,10 +200,6 @@
by_category = {}
by_day = {}
- week = {"days": [], "totals": []}
-
- facts = storage.get_facts(self.start_date, self.end_date)
-
for i in range((self.end_date - self.start_date).days + 1):
current_date = self.start_date + dt.timedelta(i)
# date format in overview window fact listing
@@ -196,6 +213,9 @@
""])
by_day[self.start_date + dt.timedelta(i)] = {"duration": 0, "row_pointer": day_row}
+
+
+
for fact in facts:
start_date = fact["start_time"].date()
@@ -216,72 +236,80 @@
fact["description"]
])
- if fact["name"] not in by_activity: by_activity[fact["name"]] = 0
- if fact["category"] not in by_category: by_category[fact["category"]] = 0
-
if duration:
by_day[start_date]["duration"] += duration
- by_activity[fact["name"]] += duration
- by_category[fact["category"]] += duration
-
- days = 30
- if self.week_view.get_active():
- days = 7
- date_sort = lambda a, b: (b[4] < a[4]) - (a[4] < b[4])
- totals["by_day"] = []
-
for day in by_day:
self.fact_store.set_value(by_day[day]["row_pointer"], 2,
stuff.format_duration(by_day[day]["duration"]))
- if (self.end_date - self.start_date).days < 20:
- strday = stuff.locale_to_utf8(day.strftime('%a'))
- totals["by_day"].append([strday, by_day[day]["duration"] / 60.0, None, None, day])
- else:
- # date format in month chart in overview window (click on "month" to see it)
- # prefix is "m_", letter after prefix is regular python format. you can use all of them
- strday = _("%(m_b)s %(m_d)s") % stuff.dateDict(day, "m_")
-
- background = None
- if day.weekday() in [5, 6]:
- background = 7
- totals["by_day"].append([strday, by_day[day]["duration"] / 60.0, None, background, day])
- totals["by_day"].sort(date_sort)
-
-
- duration_sort = lambda a, b: (a[1] < b[1]) - (b[1] < a[1])
- totals["by_activity"] = []
- for activity in by_activity:
- totals["by_activity"].append([activity, by_activity[activity] / 60.0])
- totals["by_activity"].sort(duration_sort)
-
- #now we will limit bars to 6 and sum everything else into others
- if len(totals["by_activity"]) > 12:
- other_total = 0.0
-
- for i in range(11, len(totals["by_activity"]) - 1):
- other_total += totals["by_activity"][i][1]
-
- totals["by_activity"] = totals["by_activity"][:11]
- totals["by_activity"].append([_("Other"), other_total, 1])
- totals["by_activity"].sort(duration_sort) #sort again, since maybe others summed is bigger
-
- totals["by_category"] = []
- for category in by_category:
- totals["by_category"].append([category, by_category[category] / 60.0])
- totals["by_category"].sort(duration_sort)
-
self.fact_tree.expand_all()
self.get_widget("report_button").set_sensitive(len(facts) > 0)
- week["totals"] = totals
+
+ def get_totals(self, facts, all_days):
+ # get list of used activities in interval
+ activities = [act[0] for act in
+ storage.get_interval_activity_ids(self.start_date, self.end_date)]
+
+ # get list of used categories in interval
+ categories = [cat[0] for cat in storage.get_popular_categories()]
+
+ # fill in the activity totals blanks
+ # don't want to add ability to be able to specify color per bar
+ # so we will be disguising our bar chart as multibar chart
+ activity_totals = {}
+ for act in activities:
+ activity_totals[act] = {}
+ for cat in categories:
+ activity_totals[act][cat] = 0
+
+ # fill in the category totals blanks
+ day_category_totals = {}
+ for day in all_days:
+ day_category_totals[day] = {}
+ for cat in categories:
+ day_category_totals[day][cat] = 0
+
+ #now we do the counting
+ for fact in facts:
+ duration = None
+ start_date = fact['start_time'].date()
+
+ if fact["end_time"]: # not set if just started
+ delta = fact["end_time"] - fact["start_time"]
+ duration = 24 * delta.days + delta.seconds / 60
+ elif start_date == dt.date.today():
+ delta = dt.datetime.now() - fact["start_time"]
+ duration = 24 * delta.days + delta.seconds / 60
+
+ activity_totals[fact['name']][fact['category']] += duration or 0
+ day_category_totals[start_date][fact['category']] += duration or 0
+
+
+ # convert dictionaries into lists so we don't have to care about keys anymore
+ res_categories = []
+ for day in all_days:
+ res_categories.append([day_category_totals[day][cat] / 60.0 for cat in categories])
- return week
+ #sort activities by duration, longest first
+ activity_totals = activity_totals.items()
+ activity_totals = sorted(activity_totals,
+ key = lambda(k,v): (max(v.values()), k),
+ reverse = True)
+
+ activities = [] #we have changed the order
+ res_activities = []
+ for act in activity_totals:
+ activities.append(act[0])
+ res_activities.append([act[1][cat] / 60.0 for cat in categories])
+
+ return {'keys': activities, 'values': res_activities}, \
+ {'keys': categories, 'values': res_categories}
def do_graph(self):
@@ -290,6 +318,7 @@
if self.start_date.year != self.end_date.year:
+
# overview label if start and end years don't match
# letter after prefixes (start_, end_) is the one of
# standard python date formatting ones- you can use all of them
@@ -321,16 +350,51 @@
label.set_text(overview_label)
label2 = self.get_widget("dayview_caption")
- label2.set_markup("<b>%s</b>" % (dayview_caption))
+ label2.set_markup("%s" % (dayview_caption))
+
+
+
+ fact_list = storage.get_facts(self.start_date, self.end_date)
+
+ all_days = [self.start_date + dt.timedelta(i)
+ for i in range((self.end_date - self.start_date).days + 1)]
+
+ self.get_facts(fact_list)
+ activity_totals, day_category_totals = self.get_totals(fact_list, all_days)
+
- facts = self.get_facts()
+ categories = [cat[0] for cat in storage.get_popular_categories()]
+ self.activity_chart.plot2(activity_totals['keys'],
+ activity_totals['values'],
+ series_keys = categories)
+
- self.day_chart.plot(facts["totals"]["by_day"])
- self.category_chart.plot(facts["totals"]["by_category"])
- self.activity_chart.plot(facts["totals"]["by_activity"])
+ #show days or dates depending on scale
+ if (self.end_date - self.start_date).days < 20:
+ day_keys = [day.strftime("%a") for day in all_days]
+ else:
+ day_keys = [day.strftime(_("%(m_b)s %(m_d)s") % stuff.dateDict(day, "m_")) for day in all_days]
+ self.day_chart.plot2(day_keys, day_category_totals['values'],
+ series_keys = day_category_totals['keys'])
+ category_totals = [[sum(value) for value in zip(*day_category_totals['values'])]]
+ self.category_chart.plot2([_("Total")], category_totals,
+ series_keys = day_category_totals['keys'])
+
+
+ #total string in right bottom corner, maybe temporar
+ total_string = ""
+ for i in range(len(day_category_totals['keys'])):
+ if category_totals[0][i] > 0:
+ total_string += _("%(category)s: %(duration).1f, ") % \
+ ({'category': day_category_totals['keys'][i],
+ 'duration': category_totals[0][i]})
+
+ total_string = total_string.rstrip(", ") # trailing slash
+ self.get_widget("totals").set_text(total_string);
+
def get_widget(self, name):
Modified: trunk/hamster/storage.py
==============================================================================
--- trunk/hamster/storage.py (original)
+++ trunk/hamster/storage.py Fri Feb 13 20:47:03 2009
@@ -49,6 +49,12 @@
def get_facts(self, date, end_date = None):
return self.__get_facts(date, end_date)
+ def get_popular_categories(self):
+ return self.__get_popular_categories()
+
+ def get_interval_activity_ids(self, date, end_date = None):
+ return self.__get_interval_activity_ids(date, end_date)
+
def remove_fact(self, fact_id):
fact = self.get_fact(fact_id)
if fact:
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]