def do_charts(self, facts): all_categories = self.popular_categories #the single "totals" (by category) bar category_sums = stuff.totals(facts, lambda fact: fact["category"], lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0) category_totals = [category_sums.get(cat, 0) for cat in all_categories] category_keys = ["%s %.1f" % (cat, category_sums.get(cat, 0.0)) for cat in all_categories] self.category_chart.plot([_("Total")], [category_totals], stack_keys = category_keys) # day / category chart all_days = [self.start_date + dt.timedelta(i) for i in range((self.end_date - self.start_date).days + 1)] by_date_cat = stuff.totals(facts, lambda fact: (fact["date"], fact["category"]), lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0) res = [[by_date_cat.get((day, cat), 0) for cat in all_categories] for day in all_days] #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 = [_("%(m_b)s %(m_d)s") % stuff.dateDict(day, "m_") for day in all_days] self.day_chart.plot(day_keys, res, stack_keys = all_categories) #totals by activity, disguised under a stacked bar chart to get category colors activity_sums = stuff.totals(facts, lambda fact: (fact["name"], fact["category"]), lambda fact: stuff.duration_minutes(fact["delta"])) by_duration = sorted(activity_sums.items(), key = lambda x: x[1], reverse = True) by_duration_keys = [entry[0][0] for entry in by_duration] category_sums = [[entry[1] / 60.0 * (entry[0][1] == cat) for cat in all_categories] for entry in by_duration] self.activity_chart.plot(by_duration_keys, category_sums, stack_keys = all_categories)
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(_("Still 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 self.timeline.draw(facts) # 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: by_weekday[day] = (sum([fact[0] for fact in by_weekday[day]]) / len(by_weekday[day]), sum([fact[1] for fact in by_weekday[day]]) / len(by_weekday[day])) 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: by_category[cat] = (sum([fact[0] for fact in by_category[cat]]) / len(by_category[cat]), sum([fact[1] for fact in by_category[cat]]) / len(by_category[cat])) 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 case when year has not been selected first_date = _("%(first_b)s %(first_d)s, %(first_Y)s") % \ stuff.dateDict(facts[0]["start_time"], "first_") else: #date format when year has been selected first_date = _("%(first_b)s %(first_d)s") % \ stuff.dateDict(facts[0]["start_time"], "first_") 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: summary += " " + _("""Time tracked so far is %(human_days)s human days \ (%(human_years)s years) or %(working_days)s working days \ (%(working_years)s years).""") % \ ({"human_days": ("<b>%d</b>" % total_delta.days), "human_years": ("<b>%.2f</b>" % (total_delta.days / 365.0)), "working_days": ("<b>%d</b>" % (total_delta.days * 3)), # 8 should be pretty much an average working day "working_years": ("<b>%.2f</b>" % (total_delta.days * 3 / 365.0))}) # longest fact max_fact = None for fact in facts: if not max_fact or fact["delta"] > max_fact["delta"]: max_fact = fact datedict = stuff.dateDict(max_fact["start_time"], "max_") datedict["hours"] = "<b>%.1f</b>" % (max_fact["delta"].seconds / 60 / 60.0 + max_fact["delta"].days * 24) summary += "\n" + _("Longest continuous work happened on \ %(max_b)s %(max_d)s, %(max_Y)s and was %(hours)s hours.") % datedict # total records (in selected scope) summary += " " + _("There are %s records.") % ("<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 facts 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 facts 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 tasks being shorter \ than 15 minutes you seem to be a busy bee." % ("<b>%d</b>" % short_percent)) self.explore_summary.set_text(summary)
def init_stats(self): self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today()) by_year = stuff.totals(self.stat_facts, lambda fact: fact["start_time"].year, lambda fact: 1) year_box = self.get_widget("year_box") 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(_("All"), 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() self.chart_category_totals = charting.HorizontalBarChart(value_format = "%.1f", bars_beveled = False, background = self.background, max_bar_width = 20, legend_width = 70) self.get_widget("explore_category_totals").add(self.chart_category_totals) self.chart_weekday_totals = charting.HorizontalBarChart(value_format = "%.1f", bars_beveled = False, background = self.background, max_bar_width = 20, legend_width = 70) self.get_widget("explore_weekday_totals").add(self.chart_weekday_totals) self.chart_weekday_starts_ends = charting.HorizontalDayChart(bars_beveled = False, animate = False, background = self.background, max_bar_width = 20, legend_width = 70) self.get_widget("explore_weekday_starts_ends").add(self.chart_weekday_starts_ends) self.chart_category_starts_ends = charting.HorizontalDayChart(bars_beveled = False, animate = False, background = self.background, max_bar_width = 20, legend_width = 70) self.get_widget("explore_category_starts_ends").add(self.chart_category_starts_ends) #ah, just want summary look just like all the other text on the page class CairoText(graphics.Area): def __init__(self, background = None, fontsize = 10): graphics.Area.__init__(self) self.background = background self.text = "" self.fontsize = fontsize def set_text(self, text): self.text = text self.redraw_canvas() def _render(self): if self.background: self.fill_area(0, 0, self.width, self.height, self.background) default_font = pango.FontDescription(gtk.Style().font_desc.to_string()) default_font.set_size(self.fontsize * pango.SCALE) self.layout.set_font_description(default_font) #self.context.set_source_rgb(0,0,0) self.layout.set_markup(self.text) self.layout.set_width((self.width) * pango.SCALE) self.context.move_to(0,0) charting.set_color(self.context, charting.dark[8]) self.context.show_layout(self.layout) self.explore_summary = CairoText(self.background) self.get_widget("explore_summary").add(self.explore_summary) self.get_widget("explore_summary").show_all()