def _write_fact(self, report, fact): # no having end time is fine end_time_str, end_time_iso_str = "", "" if fact.end_time: end_time_str = fact.end_time.strftime('%H:%M') end_time_iso_str = fact.end_time.isoformat() category = "" if fact.category != _("Unsorted"): #do not print "unsorted" in list category = fact.category data = dict( date=fact.date.strftime( # date column format for each row in HTML report # Using python datetime formatting syntax. See: # http://docs.python.org/library/time.html#time.strftime C_("html report", "%b %d, %Y")), date_iso=fact.date.isoformat(), activity=fact.activity, category=category, tags=fact.tags, start=fact.start_time.strftime('%H:%M'), start_iso=fact.start_time.isoformat(), end=end_time_str, end_iso=end_time_iso_str, duration=stuff.format_duration(fact.delta) or "", duration_minutes="%d" % (stuff.duration_minutes(fact.delta)), duration_decimal="%.2f" % (stuff.duration_minutes(fact.delta) / 60.0), description=fact.description or "") self.fact_rows.append( Template(self.fact_row_template).safe_substitute(data))
def init_stats(self): self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 2), dt.date.today()) if not self.stat_facts or self.stat_facts[ -1].start_time.year == self.stat_facts[0].start_time.year: self.get_widget("explore_controls").hide() else: by_year = stuff.totals(self.stat_facts, lambda fact: fact.start_time.year, lambda fact: 1) year_box = self.get_widget("year_box") if len(year_box.get_children()) == 0: class YearButton(gtk.ToggleButton): def __init__(self, label, year, on_clicked): gtk.ToggleButton.__init__(self, label) self.year = year self.connect("clicked", on_clicked) all_button = YearButton( C_("years", "All").encode("utf-8"), None, self.on_year_changed) year_box.pack_start(all_button) self.bubbling = True # TODO figure out how to properly work with togglebuttons as radiobuttons all_button.set_active(True) self.bubbling = False # TODO figure out how to properly work with togglebuttons as radiobuttons years = sorted(by_year.keys()) for year in years: year_box.pack_start( YearButton(str(year), year, self.on_year_changed)) year_box.show_all()
def _finish(self, facts): # group by date by_date = [] for date, date_facts in itertools.groupby(facts, lambda fact: fact.date): by_date.append((date, [dict(fact) for fact in date_facts])) by_date = dict(by_date) date_facts = [] date = self.start_date while date <= self.end_date: str_date = date.strftime( # date column format for each row in HTML report # Using python datetime formatting syntax. See: # http://docs.python.org/library/time.html#time.strftime C_("html report", "%b %d, %Y")) date_facts.append([str_date, by_date.get(date, [])]) date += dt.timedelta(days=1) data = dict( title=self.title, #grand_total = _("%s hours") % ("%.1f" % (total_duration.seconds / 60.0 / 60 + total_duration.days * 24)), totals_by_day_title=_("Totals by Day"), activity_log_title=_("Activity Log"), totals_title=_("Totals"), days_totals_heading=_("days"), activity_totals_heading=_("activities"), category_totals_heading=_("categories"), tag_totals_heading=_("tags"), show_prompt=_("Distinguish:"), header_date=_("Date"), header_activity=_("Activity"), header_category=_("Category"), header_tags=_("Tags"), header_start=_("Start"), header_end=_("End"), header_duration=_("Duration"), header_description=_("Description"), data_dir=runtime.data_dir, show_template=_("Show template"), template_instructions= _("You can override it by storing your version in %(home_folder)s") % {'home_folder': runtime.home_data_dir}, start_date=timegm(self.start_date.timetuple()), end_date=timegm(self.end_date.timetuple()), facts=json_dumps([dict(fact) for fact in facts]), date_facts=json_dumps(date_facts), all_activities_rows="\n".join(self.fact_rows)) self.file.write(Template(self.main_template).safe_substitute(data)) if self.override: # my report is better than your report - overrode and ran the default report trophies.unlock("my_report") return
def fill_facts_tree(self): dates = defaultdict(list) # fill blanks for i in range((self.end_date - self.start_date).days + 1): dates[self.start_date + dt.timedelta(i)] = [] #update with facts for the day for date, facts in groupby(self.facts, lambda fact: fact.date): dates[date] = list(facts) # detach model to trigger selection memory and speeding up self.fact_tree.detach_model() # push facts in tree for date, facts in sorted(dates.items(), key=lambda t: t[0]): fact_date = date.strftime(C_("overview list", "%A, %b %d")) self.fact_tree.add_group(fact_date, date, facts) self.fact_tree.attach_model()
def stats(self, year=None): facts = self.stat_facts if year: facts = filter(lambda fact: fact.start_time.year == year, facts) if not facts or (facts[-1].start_time - facts[0].start_time) < dt.timedelta(days=6): self.get_widget("statistics_box").hide() #self.get_widget("explore_controls").hide() label = self.get_widget("not_enough_records_label") if not facts: label.set_text( _("""There is no data to generate statistics yet. A week of usage would be nice!""")) else: label.set_text( _(u"Collecting data — check back after a week has passed!") ) label.show() return else: self.get_widget("statistics_box").show() self.get_widget("explore_controls").show() self.get_widget("not_enough_records_label").hide() # All dates in the scope durations = [(fact.start_time, fact.delta) for fact in facts] self.timechart.draw(durations, facts[0].date, facts[-1].date) # Totals by category categories = stuff.totals(facts, lambda fact: fact.category, lambda fact: fact.delta.seconds / 60 / 60.0) category_keys = sorted(categories.keys()) categories = [categories[key] for key in category_keys] self.chart_category_totals.plot(category_keys, categories) # Totals by weekday weekdays = stuff.totals( facts, lambda fact: (fact.start_time.weekday(), fact.start_time.strftime("%a")), lambda fact: fact.delta.seconds / 60 / 60.0) weekday_keys = sorted(weekdays.keys(), key=lambda x: x[0]) #sort weekdays = [weekdays[key] for key in weekday_keys] #get values in the order weekday_keys = [ key[1] for key in weekday_keys ] #now remove the weekday and keep just the abbreviated one self.chart_weekday_totals.plot(weekday_keys, weekdays) split_minutes = 5 * 60 + 30 #the mystical hamster midnight # starts and ends by weekday by_weekday = {} for date, date_facts in groupby(facts, lambda fact: fact.start_time.date()): date_facts = list(date_facts) weekday = (date_facts[0].start_time.weekday(), date_facts[0].start_time.strftime("%a")) by_weekday.setdefault(weekday, []) start_times, end_times = [], [] for fact in date_facts: start_time = fact.start_time.time() start_time = start_time.hour * 60 + start_time.minute if fact.end_time: end_time = fact.end_time.time() end_time = end_time.hour * 60 + end_time.minute if start_time < split_minutes: start_time += 24 * 60 if end_time < start_time: end_time += 24 * 60 start_times.append(start_time) end_times.append(end_time) if start_times and end_times: by_weekday[weekday].append((min(start_times), max(end_times))) for day in by_weekday: n = len(by_weekday[day]) # calculate mean and variance for starts and ends means = (sum([fact[0] for fact in by_weekday[day]]) / n, sum([fact[1] for fact in by_weekday[day]]) / n) variances = (sum([(fact[0] - means[0])**2 for fact in by_weekday[day]]) / n, sum([(fact[1] - means[1])**2 for fact in by_weekday[day]]) / n) # In the normal distribution, the range from # (mean - standard deviation) to infinit, or from # -infinit to (mean + standard deviation), has an accumulated # probability of 84.1%. Meaning we are using the place where if we # picked a random start(or end), 84.1% of the times it will be # inside the range. by_weekday[day] = (int(means[0] - math.sqrt(variances[0])), int(means[1] + math.sqrt(variances[1]))) min_weekday = min([by_weekday[day][0] for day in by_weekday]) max_weekday = max([by_weekday[day][1] for day in by_weekday]) weekday_keys = sorted(by_weekday.keys(), key=lambda x: x[0]) weekdays = [by_weekday[key] for key in weekday_keys] weekday_keys = [key[1] for key in weekday_keys ] # get rid of the weekday number as int # starts and ends by category by_category = {} for date, date_facts in groupby(facts, lambda fact: fact.start_time.date()): date_facts = sorted(list(date_facts), key=lambda x: x.category) for category, category_facts in groupby(date_facts, lambda x: x.category): category_facts = list(category_facts) by_category.setdefault(category, []) start_times, end_times = [], [] for fact in category_facts: start_time = fact.start_time start_time = start_time.hour * 60 + start_time.minute if fact.end_time: end_time = fact.end_time.time() end_time = end_time.hour * 60 + end_time.minute if start_time < split_minutes: start_time += 24 * 60 if end_time < start_time: end_time += 24 * 60 start_times.append(start_time) end_times.append(end_time) if start_times and end_times: by_category[category].append( (min(start_times), max(end_times))) for cat in by_category: # For explanation see the comments in the starts and ends by day n = len(by_category[cat]) means = (sum([fact[0] for fact in by_category[cat]]) / n, sum([fact[1] for fact in by_category[cat]]) / n) variances = (sum([(fact[0] - means[0])**2 for fact in by_category[cat]]) / n, sum([(fact[1] - means[1])**2 for fact in by_category[cat]]) / n) by_category[cat] = (int(means[0] - math.sqrt(variances[0])), int(means[1] + math.sqrt(variances[1]))) min_category = min([by_category[day][0] for day in by_category]) max_category = max([by_category[day][1] for day in by_category]) category_keys = sorted(by_category.keys(), key=lambda x: x[0]) categories = [by_category[key] for key in category_keys] #get starting and ending hours for graph and turn them into exact hours that divide by 3 min_hour = min([min_weekday, min_category]) / 60 * 60 max_hour = max([max_weekday, max_category]) / 60 * 60 self.chart_weekday_starts_ends.plot_day(weekday_keys, weekdays, min_hour, max_hour) self.chart_category_starts_ends.plot_day(category_keys, categories, min_hour, max_hour) #now the factoids! summary = "" # first record if not year: # date format for the first record if the year has not been selected # Using python datetime formatting syntax. See: # http://docs.python.org/library/time.html#time.strftime first_date = facts[0].start_time.strftime( C_("first record", "%b %d, %Y")) else: # date of first record when year has been selected # Using python datetime formatting syntax. See: # http://docs.python.org/library/time.html#time.strftime first_date = facts[0].start_time.strftime( C_("first record", "%b %d")) summary += _("First activity was recorded on %s.") % \ ("<b>%s</b>" % first_date) # total time tracked total_delta = dt.timedelta(days=0) for fact in facts: total_delta += fact.delta if total_delta.days > 1: human_years_str = ngettext( "%(num)s year", "%(num)s years", total_delta.days / 365) % { 'num': "<b>%s</b>" % locale.format("%.2f", (total_delta.days / 365.0)) } working_years_str = ngettext( "%(num)s year", "%(num)s years", total_delta.days * 3 / 365) % { 'num': "<b>%s</b>" % locale.format("%.2f", (total_delta.days * 3 / 365.0)) } #FIXME: difficult string to properly pluralize summary += " " + _( """Time tracked so far is %(human_days)s human days \ (%(human_years)s) or %(working_days)s working days (%(working_years)s).""") % { "human_days": ("<b>%d</b>" % total_delta.days), "human_years": human_years_str, "working_days": ("<b>%d</b>" % (total_delta.days * 3) ), # 8 should be pretty much an average working day "working_years": working_years_str } # longest fact max_fact = None for fact in facts: if not max_fact or fact.delta > max_fact.delta: max_fact = fact longest_date = max_fact.start_time.strftime( # How the date of the longest activity should be displayed in statistics # Using python datetime formatting syntax. See: # http://docs.python.org/library/time.html#time.strftime C_("date of the longest activity", "%b %d, %Y")) num_hours = max_fact.delta.seconds / 60 / 60.0 + max_fact.delta.days * 24 hours = "<b>%s</b>" % locale.format("%.1f", num_hours) summary += "\n" + ngettext( "Longest continuous work happened on \ %(date)s and was %(hours)s hour.", "Longest continuous work happened on \ %(date)s and was %(hours)s hours.", int(num_hours)) % { "date": longest_date, "hours": hours } # total records (in selected scope) summary += " " + ngettext("There is %s record.", "There are %s records.", len(facts)) % ("<b>%d</b>" % len(facts)) early_start, early_end = dt.time(5, 0), dt.time(9, 0) late_start, late_end = dt.time(20, 0), dt.time(5, 0) fact_count = len(facts) def percent(condition): matches = [fact for fact in facts if condition(fact)] return round(len(matches) / float(fact_count) * 100) early_percent = percent( lambda fact: early_start < fact.start_time.time() < early_end) late_percent = percent(lambda fact: fact.start_time.time() > late_start or fact.start_time.time() < late_end) short_percent = percent( lambda fact: fact.delta <= dt.timedelta(seconds=60 * 15)) if fact_count < 100: summary += "\n\n" + _( "Hamster would like to observe you some more!") elif early_percent >= 20: summary += "\n\n" + _( "With %s percent of all activities starting before \ 9am, you seem to be an early bird.") % ("<b>%d</b>" % early_percent) elif late_percent >= 20: summary += "\n\n" + _( "With %s percent of all activities starting after \ 11pm, you seem to be a night owl.") % ("<b>%d</b>" % late_percent) elif short_percent >= 20: summary += "\n\n" + _( "With %s percent of all activities being shorter \ than 15 minutes, you seem to be a busy bee.") % ("<b>%d</b>" % short_percent) self.explore_summary.set_text(summary)