[hamster-applet] merging timeline widget back
- From: Toms Baugis <tbaugis src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [hamster-applet] merging timeline widget back
- Date: Wed, 23 Dec 2009 02:27:11 +0000 (UTC)
commit 1ccf6c086af456b3a4ec9c5d1b6a3b21bfb7a13f
Author: Toms Bauģis <toms baugis gmail com>
Date: Wed Dec 23 01:52:47 2009 +0000
merging timeline widget back
hamster/stats.py | 14 +-
hamster/stats_stats.py | 2 +-
hamster/widgets/Makefile.am | 1 -
hamster/widgets/__init__.py | 1 -
hamster/widgets/newtimeline.py | 341 ---------------------------------
hamster/widgets/timeline.py | 411 ++++++++++++++++++++++++++++------------
po/POTFILES.in | 2 +-
7 files changed, 301 insertions(+), 471 deletions(-)
---
diff --git a/hamster/stats.py b/hamster/stats.py
index 39b834c..4e1f197 100644
--- a/hamster/stats.py
+++ b/hamster/stats.py
@@ -92,7 +92,7 @@ class StatsViewer(object):
self.end_date_input.connect("date-entered", self.on_end_date_entered)
self.get_widget("range_end_box").add(self.end_date_input)
- self.timeline = widgets.NewTimeLine()
+ self.timeline = widgets.TimeLine()
self.get_widget("by_day_box").add(self.timeline)
self._gui.connect_signals(self)
@@ -134,9 +134,14 @@ class StatsViewer(object):
self.get_widget("report_button").set_sensitive(len(facts) > 0)
self.timeline.draw(facts, self.start_date, self.end_date)
-
- self.overview.search(self.start_date, self.end_date, facts)
- self.reports.search(self.start_date, self.end_date, facts)
+
+
+ if self.get_widget("window_tabs").get_current_page() == 0:
+ self.overview.search(self.start_date, self.end_date, facts)
+ self.reports.search(self.start_date, self.end_date, facts)
+ else:
+ self.reports.search(self.start_date, self.end_date, facts)
+ self.overview.search(self.start_date, self.end_date, facts)
def on_search_activate(self, widget):
self.search()
@@ -230,7 +235,6 @@ class StatsViewer(object):
elif pagenum == 1:
self.get_widget('remove').set_sensitive(False)
self.get_widget('edit').set_sensitive(False)
- self.reports.do_graph(self.facts)
def on_add_clicked(self, button):
diff --git a/hamster/stats_stats.py b/hamster/stats_stats.py
index eca6880..0cd79db 100644
--- a/hamster/stats_stats.py
+++ b/hamster/stats_stats.py
@@ -186,7 +186,7 @@ A week of usage would be nice!"""))
self.get_widget("not_enough_records_label").hide()
# All dates in the scope
- self.timeline.draw(facts)
+ self.timeline.draw(facts, facts[0]["date"], facts[-1]["date"])
# Totals by category
diff --git a/hamster/widgets/Makefile.am b/hamster/widgets/Makefile.am
index 3027dba..02ab977 100644
--- a/hamster/widgets/Makefile.am
+++ b/hamster/widgets/Makefile.am
@@ -5,7 +5,6 @@ hamster_PYTHON = \
dateinput.py \
dayline.py \
facttree.py \
- newtimeline.py \
reportchooserdialog.py \
tags.py \
timeinput.py \
diff --git a/hamster/widgets/__init__.py b/hamster/widgets/__init__.py
index a843695..ab3b9a2 100644
--- a/hamster/widgets/__init__.py
+++ b/hamster/widgets/__init__.py
@@ -26,7 +26,6 @@ from dateinput import DateInput
from timeinput import TimeInput
from timeline import TimeLine
-from newtimeline import NewTimeLine
from dayline import DayLine
diff --git a/hamster/widgets/timeline.py b/hamster/widgets/timeline.py
index 248891f..83b901f 100644
--- a/hamster/widgets/timeline.py
+++ b/hamster/widgets/timeline.py
@@ -17,156 +17,325 @@
# You should have received a copy of the GNU General Public License
# along with Project Hamster. If not, see <http://www.gnu.org/licenses/>.
-from .hamster import graphics
+import gtk, pango
+
+from .hamster import graphics, stuff
+
+from .hamster.configuration import GconfStore
+
import datetime as dt
+import calendar
+
+from bisect import bisect
+
+HOUR = dt.timedelta(seconds = 60*60)
+DAY = dt.timedelta(1)
+WEEK = dt.timedelta(7)
+MONTH = dt.timedelta(30)
class TimeLine(graphics.Area):
"""this widget is kind of half finished"""
- MODE_YEAR = 0
- MODE_MONTH = 1
- MODE_WEEK = 1
- MODE_DAY = 3
def __init__(self):
graphics.Area.__init__(self)
- self.start_date, self.end_date = None, None
- self.draw_mode = None
- self.max_hours = None
-
-
- # Normal stuff
- def draw(self, facts):
- import itertools
- self.facts = {}
- for date, date_facts in itertools.groupby(facts, lambda x: x["start_time"].date()):
- date_facts = list(date_facts)
- self.facts[date] = date_facts
- self.max_hours = max(self.max_hours,
- sum([fact["delta"].seconds / 60 / float(60) +
- fact["delta"].days * 24 for fact in date_facts]))
-
- start_date = facts[0]["start_time"].date()
- end_date = facts[-1]["start_time"].date()
-
- self.draw_mode = self.MODE_YEAR
- self.start_date = start_date.replace(month=1, day=1)
- self.end_date = end_date.replace(month=12, day=31)
-
-
- """
- #TODO - for now we have only the year mode
- if start_date.year != end_date.year or start_date.month != end_date.month:
- self.draw_mode = self.MODE_YEAR
- self.start_date = start_date.replace(month=1, day=1)
- self.end_date = end_date.replace(month=12, day=31)
- elif start_date.strftime("%W") != end_date.strftime("%W"):
- self.draw_mode = self.MODE_MONTH
- self.start_date = start_date.replace(day=1)
- self.end_date = end_date.replace(date =
- calendar.monthrange(self.end_date.year,
- self.end_date.month)[1])
- elif start_date != end_date:
- self.draw_mode = self.MODE_WEEK
- else:
- self.draw_mode = self.MODE_DAY
- """
+ self.start_time, self.end_time = None, None
+ self.facts = []
+ self.title = ""
+ self.day_start = GconfStore().get_day_start()
+ self.first_weekday = stuff.locale_first_weekday()
- self.redraw_canvas()
+ self.minor_tick = None
+
+ self.tick_totals = []
+
+
+ def draw(self, facts, start_date, end_date):
+ self.facts = facts
+
+ if start_date > end_date:
+ start_date, end_date = end_date, start_date
+
+ self.set_title(start_date, end_date) # we will forget about all our magic manipulations for the title
+
+ # for hourly representation we will operate in minutes since and until the day start
+ if end_date - start_date < dt.timedelta(days=2):
+ start_time = dt.datetime.combine(start_date, self.day_start.replace(minute=0))
+ end_time = dt.datetime.combine(end_date, self.day_start.replace(minute=0)) + dt.timedelta(days = 1)
+
+ fact_start_time, fact_end_time = start_time, end_time
+ if facts:
+ fact_start_time = facts[0]["start_time"]
+ fact_end_time = facts[-1]["start_time"] + facts[-1]["delta"]
+
+ self.start_time = min([start_time, fact_start_time])
+ self.end_time = max([end_time, fact_end_time])
+
+ else:
+ start_time = dt.datetime.combine(start_date, dt.time())
+ end_time = dt.datetime.combine(end_date, dt.time(23, 59))
+
+ fact_start_time, fact_end_time = start_time, end_time
+ if facts:
+ fact_start_time = dt.datetime.combine(facts[0]["date"], dt.time())
+ fact_end_time = dt.datetime.combine(facts[-1]["date"], dt.time())
+
+ self.start_time = min([start_time, fact_start_time])
+ self.end_time = max([end_time, fact_end_time])
+
+
+
+ days = (self.end_time - self.start_time).days
+
+ # determine fraction and do addittional start time move
+ if days > 125: # about 4 month -> show per month
+ self.minor_tick = dt.timedelta(days = 30) #this is approximate and will be replaced by exact days in month
+ # make sure we start on first day of month
+ self.start_time = self.start_time - dt.timedelta(self.start_time.day - 1)
+
+ elif days > 40: # bit more than month -> show per week
+ self.minor_tick = WEEK
+ # make sure we start week on first day
+ #set to monday
+ self.start_time = self.start_time - dt.timedelta(self.start_time.weekday() + 1)
+ # look if we need to start on sunday or monday
+ self.start_time = self.start_time + dt.timedelta(self.first_weekday)
+ elif days > 2: # more than two days -> show per day
+ self.minor_tick = dt.timedelta(days = 1)
+ else: # show per hour
+ self.minor_tick = dt.timedelta(seconds = 60 * 60)
+
+ self.count_hours()
+ self.redraw_canvas()
+
+
def on_expose(self):
- import calendar
+ self.context.set_line_width(1)
+
+ self.fill_area(0, 0, self.width, self.height, "#fafafa")
+ self.context.stroke()
+ self.rectangle(0, 0, self.width, self.height, "#666666")
+ self.context.stroke()
- if self.draw_mode != self.MODE_YEAR:
+ self.height = self.height - 2
+ graph_x = 2
+ graph_width = self.width - graph_x - 2
+
+ if not self.facts:
return
+
+ total_minutes = stuff.duration_minutes(self.end_time - self.start_time)
+ bar_width = float(graph_width) / len(self.tick_totals)
- self.fill_area(0, 0, self.width, self.height, (0.975,0.975,0.975))
- self.set_color((100,100,100))
+ # calculate position of each bar
+ # essentially we care more about the exact 1px gap between bars than about the bar width
+ # so after each iteration, we adjust the bar width
+ x = graph_x
+ exes = {}
+ adapted_bar_width = bar_width
+ for i, (current_time, total) in enumerate(self.tick_totals):
+ exes[current_time] = (x, round(adapted_bar_width)) #saving those as getting pixel precision is not an exact science
+ x = round(x + adapted_bar_width)
+ adapted_bar_width = (self.width - x) / float(max(len(self.tick_totals) - i - 1, 1))
- days = (self.end_date - self.start_date).days
- pixels_in_day = self.width / float(days)
- month_label_fits = True
- for month in range(1, 13):
- self.layout.set_text(calendar.month_abbr[month])
- label_w, label_h = self.layout.get_pixel_size()
- if label_w * 2 > pixels_in_day * 30:
- month_label_fits = False
- break
-
+
+ # major ticks
+ all_times = [tick[0] for tick in self.tick_totals]
+
+ if self.end_time - self.start_time < dt.timedelta(days=3): # about the same day
+ major_step = dt.timedelta(seconds = 60 * 60)
+ else:
+ major_step = dt.timedelta(days=1)
- ticker_date = self.start_date
+ x = graph_x
+ major_tick_step = graph_width / (total_minutes / float(stuff.duration_minutes(major_step)))
+ current_time = self.start_time
- year_pos = 0
+ def line(x, color):
+ self.context.move_to(round(x) + 0.5, 0)
+ self.set_color(color)
+ self.context.line_to(round(x) + 0.5, self.height)
+ self.context.stroke()
+
+ def somewhere_in_middle(time, color):
+ # draws line somewhere in middle of the minor tick
+ left_index = exes.keys()[bisect(exes.keys(), time) - 1]
+ #should yield something between 0 and 1
+ adjustment = stuff.duration_minutes(time - left_index) / float(stuff.duration_minutes(self.minor_tick))
+ x, width = exes[left_index]
+ line(x + round(width * adjustment) - 1, color)
- for year in range(self.start_date.year, self.end_date.year + 1):
- #due to how things lay over, we are putting labels on backwards, so that they don't overlap
+ while current_time < self.end_time:
+ current_time += major_step
+ x += major_tick_step
- self.context.set_line_width(1)
- for month in range(1, 13):
- for day in range(1, calendar.monthrange(year, month)[1] + 1):
- ticker_pos = (year_pos + ticker_date.timetuple().tm_yday * pixels_in_day)
-
-
-
- if pixels_in_day > 5:
- self.context.move_to(ticker_pos, self.height - 20)
- self.context.line_to(ticker_pos, self.height)
-
- self.layout.set_text(ticker_date.strftime("%d"))
- label_w, label_h = self.layout.get_pixel_size()
-
- if label_w < pixels_in_day / 1.2: #if label fits
- self.context.move_to(ticker_pos + 2, self.height - 20)
- self.context.show_layout(self.layout)
-
- self.context.stroke()
-
- #now facts
- facts_today = self.facts.get(ticker_date, [])
- if facts_today:
- total_length = dt.timedelta()
- for fact in facts_today:
- total_length += fact["delta"]
- total_length = total_length.seconds / 60 / 60.0 + total_length.days * 24
- total_length = total_length / float(self.max_hours) * self.height - 16
-
- self.fill_area(round(ticker_pos),
- round(self.height - total_length),
- round(pixels_in_day),
- round(total_length),
- (190,190,190))
-
-
-
-
- ticker_date += dt.timedelta(1)
-
+ if current_time >= self.end_time: # TODO - fix the loop so we don't have to break
+ break
-
- if month_label_fits:
- #roll back a little
- month_pos = ticker_pos - (calendar.monthrange(year, month)[1] + 1) * pixels_in_day
+ if major_step < DAY: # about the same day
+ if current_time.time() == dt.time(0,0): # midnight
+ line(exes[current_time][0] - 1, "#aaaaaa")
+ else:
+ if self.minor_tick == DAY: # week change
+ if current_time.weekday() == 0:
+ line(exes[current_time][0] - 1, "#cccccc")
+
+ if self.minor_tick <= WEEK: # month change
+ if current_time.day == 1:
+ if current_time in exes:
+ line(exes[current_time][0] - 1, "#999999")
+ else: #if we are somewhere in middle then it gets a bit more complicated
+ somewhere_in_middle(current_time, "#999999")
+
+ # year change
+ if current_time.timetuple().tm_yday == 1: # year change
+ if current_time in exes:
+ line(exes[current_time][0] - 1, "#00ff00")
+ else: #if we are somewhere in middle - then just draw it
+ somewhere_in_middle(current_time, "#00ff00")
- self.context.move_to(month_pos, 0)
- self.layout.set_text(dt.date(year, month, 1).strftime("%b"))
- self.context.show_layout(self.layout)
-
+ # the bars
+ for i, (current_time, total) in enumerate(self.tick_totals):
+ bar_size = max(round(self.height * total * 0.9), 1)
+ x, bar_width = exes[current_time]
+
+ self.fill_area(x, self.height - bar_size, min(bar_width - 1, self.width - x - 2), bar_size, "#eeeeee")
+
+
+
+ #minor tick format
+ if self.minor_tick >= dt.timedelta(days = 28): # month
+ step_format = "%b"
+
+ elif self.minor_tick == WEEK: # week
+ step_format = "%b %d"
+ elif self.minor_tick == DAY: # day
+ if (self.end_time - self.start_time) > dt.timedelta(10):
+ step_format = "%b %d"
+ else:
+ step_format = "%a"
+ else:
+ step_format = "%H<small><sup>%M</sup></small>"
+
+
+ # ticks. we loop once again to avoid next bar overlapping previous text
+ for i, (current_time, total) in enumerate(self.tick_totals):
+ if (self.end_time - self.start_time) > dt.timedelta(10) \
+ and self.minor_tick == DAY and current_time.weekday() != 0:
+ continue
-
- self.layout.set_text("%d" % year)
- label_w, label_h = self.layout.get_pixel_size()
-
- self.context.move_to(year_pos + 2, month_label_fits * label_h * 1.2)
-
+ x, bar_width = exes[current_time]
+
+ self.set_color("#aaaaaa")
+ self.layout.set_width(int((self.width - x) * pango.SCALE))
+ self.layout.set_markup(current_time.strftime(step_format))
+ w, h = self.layout.get_pixel_size()
+
+ self.context.move_to(x + 2, self.height - h - 2)
self.context.show_layout(self.layout)
+
+
+ self.layout.set_width(-1)
+ self.set_color("#aaaaaa")
+ self.context.move_to(1, 1)
+ font = pango.FontDescription(gtk.Style().font_desc.to_string())
+ font.set_size(14 * pango.SCALE)
+ font.set_weight(pango.WEIGHT_BOLD)
+ self.layout.set_font_description(font)
+
+ self.layout.set_markup(self.title)
+
+ self.context.show_layout(self.layout)
+
+
+ def count_hours(self):
+ #go through facts and make array of time used by our fraction
+ fractions = []
+
+ current_time = self.start_time
+
+ minor_tick = self.minor_tick
+ while current_time <= self.end_time:
+ # if minor tick is month, the starting date will have been
+ # already adjusted to the first
+ # now we have to make sure to move month by month
+ if self.minor_tick >= dt.timedelta(days=28):
+ minor_tick = dt.timedelta(calendar.monthrange(current_time.year, current_time.month)[1]) # days in month
- self.context.stroke()
+ fractions.append(current_time)
+ current_time += minor_tick
+
+ hours = [0] * len(fractions)
+
+ tick_minutes = float(stuff.duration_minutes(self.minor_tick))
+
+ for fact in self.facts:
+ if self.minor_tick < dt.timedelta(1):
+ end_time = fact["start_time"] + fact["delta"] # the thing about ongoing task - it has no end time
+
+ # find in which fraction the fact starts and
+ # add duration up to the border of tick to that fraction
+ # then move cursor to the start of next fraction
+ first_index = bisect(fractions, fact["start_time"]) - 1
+ step_time = fractions[first_index]
+ first_end = min(end_time, step_time + self.minor_tick)
+ first_tick = stuff.duration_minutes(first_end - fact["start_time"]) / tick_minutes
+
+ hours[first_index] += first_tick
+ step_time = step_time + self.minor_tick
+
+ # now go through ticks until we reach end of the time
+ while step_time < end_time:
+ index = bisect(fractions, step_time) - 1
+ interval = min([1, stuff.duration_minutes(end_time - step_time) / tick_minutes])
+ hours[index] += interval
+
+ step_time += self.minor_tick
+ else:
+ hour_index = bisect(fractions, dt.datetime.combine(fact["date"], dt.time())) - 1
+ hours[hour_index] += stuff.duration_minutes(fact["delta"])
+
+ # now normalize
+ max_hour = max(hours)
+ hours = [hour / float(max_hour or 1) for hour in hours]
- year_pos = ticker_pos #save current state for next year
+ self.tick_totals = zip(fractions, hours)
+
+
+ def set_title(self, start_date, end_date):
+ dates_dict = stuff.dateDict(start_date, "start_")
+ dates_dict.update(stuff.dateDict(end_date, "end_"))
+
+ if start_date == end_date:
+ # date format for overview label when only single day is visible
+ # Using python datetime formatting syntax. See:
+ # http://docs.python.org/library/time.html#time.strftime
+ start_date_str = start_date.strftime(_("%B %d, %Y"))
+ # Overview label if looking on single day
+ self.title = start_date_str
+ elif start_date.year != 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
+ # see http://docs.python.org/library/time.html#time.strftime
+ self.title = _(u"%(start_B)s %(start_d)s, %(start_Y)s â?? %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
+ elif start_date.month != end_date.month:
+ # overview label if start and end month do not match
+ # letter after prefixes (start_, end_) is the one of
+ # standard python date formatting ones- you can use all of them
+ # see http://docs.python.org/library/time.html#time.strftime
+ self.title = _(u"%(start_B)s %(start_d)s â?? %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
+ else:
+ # overview label for interval in same month
+ # letter after prefixes (start_, end_) is the one of
+ # standard python date formatting ones- you can use all of them
+ # see http://docs.python.org/library/time.html#time.strftime
+ self.title = _(u"%(start_B)s %(start_d)s â?? %(end_d)s, %(end_Y)s") % dates_dict
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 308455c..b5fbde1 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -22,4 +22,4 @@ hamster/stats_reports.py
hamster/stats_stats.py
hamster/stuff.py
hamster/widgets/reportchooserdialog.py
-hamster/widgets/newtimeline.py
+hamster/widgets/timeline.py
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]