diff --git a/src/review_heatmap/activity.py b/src/review_heatmap/activity.py index d00f176..1b8cd75 100644 --- a/src/review_heatmap/activity.py +++ b/src/review_heatmap/activity.py @@ -33,8 +33,12 @@ Components related to gathering and analyzing user activity """ -from __future__ import (absolute_import, division, - print_function, unicode_literals) +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) import time import datetime @@ -50,7 +54,6 @@ class ActivityReporter(object): - def __init__(self, col, config, whole=False): self.col = col self.config = config @@ -70,8 +73,7 @@ def getData(self, limhist=None, limfcst=None, mode="reviews"): if mode == "reviews": return self._getActivity(**self._reviewsData(time_limits)) else: - raise NotImplementedError( - "activity mode {} not implemented".format(mode)) + raise NotImplementedError("activity mode {} not implemented".format(mode)) # Activity calculations ######################################################################### @@ -88,14 +90,14 @@ def _getActivity(self, history, forecast={}): # Stats: cumulative activity and streaks streak_max = streak_cur = streak_last = 0 - current = total = 0 + current = total = total_time_spent = 0 for idx, item in enumerate(history): current += 1 - timestamp, activity = item + timestamp, activity, time_spent = item try: - next_timestamp = history[idx+1][0] + next_timestamp = history[idx + 1][0] except IndexError: # last item streak_last = current next_timestamp = None @@ -106,6 +108,7 @@ def _getActivity(self, history, forecast={}): current = 0 total += activity + total_time_spent += time_spent days_learned = idx + 1 @@ -116,6 +119,8 @@ def _getActivity(self, history, forecast={}): # Stats: average count on days with activity avg_cur = int(round(total / max(days_learned, 1))) + # Stats: average time spent per days reviewing cards + time_spent_daily_avg = int(round(total_time_spent / max(days_learned, 1))) # Stats: percentage of days with activity # @@ -130,8 +135,8 @@ def _getActivity(self, history, forecast={}): else: pdays = int(round((days_learned / days_total) * 100)) - # Compose activity data - activity = dict(history + forecast) + # Compose activity data (remove time spent data to match forecast size) + activity = dict([i[:2] for i in history] + forecast) if history[-1][0] == self.today: # history takes precedence for today activity[self.today] = history[-1][1] @@ -143,23 +148,16 @@ def _getActivity(self, history, forecast={}): "today": self.today * 1000, "offset": self.offset, "stats": { - "streak_max": { - "type": "streak", - "value": streak_max - }, - "streak_cur": { - "type": "streak", - "value": streak_cur - }, - "pct_days_active": { - "type": "percentage", - "value": pdays + "streak_max": {"type": "streak", "value": streak_max}, + "streak_cur": {"type": "streak", "value": streak_cur}, + "pct_days_active": {"type": "percentage", "value": pdays}, + "activity_daily_avg": {"type": "cards", "value": avg_cur}, + "time_spent_max": {"type": "time_day", "value": total_time_spent,}, + "time_spent_daily_avg": { + "type": "time_minute", + "value": time_spent_daily_avg, }, - "activity_daily_avg": { - "type": "cards", - "value": avg_cur - } - } + }, } # Mode-specific @@ -167,8 +165,7 @@ def _getActivity(self, history, forecast={}): def _reviewsData(self, time_limits): return { "history": self._cardsDone(start=time_limits[0]), - "forecast": self._cardsDue(start=self.today, - stop=time_limits[1]) + "forecast": self._cardsDue(start=self.today, stop=time_limits[1]), } # Collection properties @@ -198,9 +195,9 @@ def daystartEpoch(timestr, is_timestamp=True, offset=0): cmd = """ SELECT CAST(STRFTIME('%s', '{timestr}', {unixepoch} {offset} -'localtime', 'start of day') AS int)""".format(timestr=timestr, - unixepoch=unixepoch, - offset=offset) +'localtime', 'start of day') AS int)""".format( + timestr=timestr, unixepoch=unixepoch, offset=offset + ) return mw.col.db.scalar(cmd) def _getToday(self, offset): @@ -218,14 +215,12 @@ def _getTimeLimits(self, limhist=None, limfcst=None): if limhist is not None: history_start = self._daysFromToday(-limhist) else: - history_start = self._getConfHistoryLimit( - conf["limhist"], conf["limdate"]) + history_start = self._getConfHistoryLimit(conf["limhist"], conf["limdate"]) if limfcst is not None: forecast_stop = self._daysFromToday(limfcst) else: - forecast_stop = self._getConfForecastLimit( - conf["limfcst"]) + forecast_stop = self._getConfForecastLimit(conf["limfcst"]) return (history_start, forecast_stop) @@ -240,8 +235,7 @@ def _getConfHistoryLimit(self, limit_days, limit_date): limit_date = self.daystartEpoch(limit_date) if limit_date else None - if (not limit_date or - limit_date == self.daystartEpoch(self.col.crt)): + if not limit_date or limit_date == self.daystartEpoch(self.col.crt): # ignore zero value or default value limit_date = 0 else: @@ -270,8 +264,7 @@ def _validDecks(self, excluded): all_excluded.extend(excluded) - return [d['id'] for d in self.col.decks.all() - if d['id'] not in all_excluded] + return [d["id"] for d in self.col.decks.all() if d["id"] not in all_excluded] def _didLimit(self): excluded_dids = self.config["synced"]["limdecks"] @@ -279,7 +272,7 @@ def _didLimit(self): if excluded_dids: dids = self._validDecks(excluded_dids) else: - dids = [d['id'] for d in self.col.decks.all()] + dids = [d["id"] for d in self.col.decks.all()] else: dids = self.col.decks.active() return ids2str(dids) @@ -299,8 +292,7 @@ def _revlogLimit(self): return "" else: dids = self.col.decks.active() - return ("cid IN (SELECT id FROM cards WHERE did IN %s)" % - ids2str(dids)) + return "cid IN (SELECT id FROM cards WHERE did IN %s)" % ids2str(dids) # Database queries for user activity ######################################################################### @@ -322,7 +314,9 @@ def _cardsDue(self, start=None, stop=None): FROM cards WHERE did IN {} AND queue IN (2,3) {} -GROUP BY day ORDER BY day""".format(self.offset, self._didLimit(), lim) +GROUP BY day ORDER BY day""".format( + self.offset, self._didLimit(), lim + ) res = self.col.db.all(cmd, today=self.col.sched.today) @@ -339,13 +333,20 @@ def _cardsDue(self, start=None, stop=None): logger.debug(self.col.sched.today) logger.debug("Anki20 %s, Scheduler version %s", ANKI20, schedver) logger.debug("Day starts at setting: %s hours", offset) - logger.debug(time.strftime("dayCutoff: %Y-%m-%d %H:%M", - time.localtime(mw.col.sched.dayCutoff))) - logger.debug(time.strftime("local now: %Y-%m-%d %H:%M", - time.localtime(time.time()))) - logger.debug(time.strftime("Col today: %Y-%m-%d", - time.localtime(mw.col.crt + - 86400 * mw.col.sched.today))) + logger.debug( + time.strftime( + "dayCutoff: %Y-%m-%d %H:%M", time.localtime(mw.col.sched.dayCutoff), + ) + ) + logger.debug( + time.strftime("local now: %Y-%m-%d %H:%M", time.localtime(time.time())) + ) + logger.debug( + time.strftime( + "Col today: %Y-%m-%d", + time.localtime(mw.col.crt + 86400 * mw.col.sched.today), + ) + ) logger.debug("Col days: %s", mw.col.sched.today) logger.debug(res) @@ -383,8 +384,10 @@ def _cardsDone(self, start=None): cmd = """ SELECT CAST(STRFTIME('%s', id / 1000 - {}, 'unixepoch', 'localtime', 'start of day') AS int) -AS day, COUNT() +AS day, COUNT(), SUM(time) as time_spent FROM revlog {} -GROUP BY day ORDER BY day""".format(offset, lim) +GROUP BY day ORDER BY day""".format( + offset, lim + ) return self.col.db.all(cmd) diff --git a/src/review_heatmap/heatmap.py b/src/review_heatmap/heatmap.py index bfdb1d0..1e7efc1 100644 --- a/src/review_heatmap/heatmap.py +++ b/src/review_heatmap/heatmap.py @@ -61,12 +61,27 @@ class HeatmapCreator(object): def compress_levels(colors, indices): return [colors[i] for i in indices] + MINUTE = 60 * 1000 # milliseconds + DAY = 24 * 60 * MINUTE # milliseconds + stat_levels = { # tuples of threshold value, css_colors index "streak": list(zip((0, 14, 30, 90, 180, 365), compress_levels(css_colors, (0, 2, 4, 6, 9, 10)))), "percentage": list(zip((0, 25, 50, 60, 70, 80, 85, 90, 95, 99), css_colors)), + "time_minute": list( + zip( + (0, 30 * MINUTE, 40 * MINUTE, 50 * MINUTE, 60 * MINUTE, 70 * MINUTE), + compress_levels(css_colors, (0, 2, 4, 6, 9, 10)), + ) + ), + "time_day": list( + zip( + (0, DAY, 3 * DAY, 10 * DAY, 30 * DAY, 100 * DAY), + compress_levels(css_colors, (0, 2, 4, 6, 9, 10)), + ) + ), } legend_factors = (0.125, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 4) @@ -74,7 +89,9 @@ def compress_levels(colors, indices): stat_units = { "streak": "day", "percentage": None, - "cards": "card" + "cards": "card", + "time_day": "hours", + "time_minute": "minutes", } def __init__(self, config, whole=False): @@ -101,7 +118,7 @@ def generate(self, view="deckbrowser", limhist=None, limfcst=None): heatmap = self._generateHeatmapElm(data, heatmap_legend) else: classes.append("rh-disable-heatmap") - + if prefs["display"][view] or prefs["statsvis"]: stats = self._generateStatsElm(data, stats_legend) else: @@ -182,7 +199,7 @@ def _heatmapLegend(self, legend): # implement different color schemes for past and future without # having to modify cal-heatmap: return [-i for i in legend[::-1]] + [0] + legend - + def _dynamicLegend(self, average): # set default average if average too low for informational levels avg = max(20, average) @@ -191,6 +208,14 @@ def _dynamicLegend(self, average): def _dayS(self, count, term): if not term: return count + if term == "hours": + hours = count // 3600000 + minutes = count % 3600000 // 60000 + return f"{hours}h {minutes}min" + elif term == "minutes": + minutes = count % 3600000 // 60000 + return f"{minutes}min" + return "{} {}{}".format( str(count), term, "s" if abs(count) > 1 else "" diff --git a/src/review_heatmap/web.py b/src/review_heatmap/web.py index 990c328..0678590 100644 --- a/src/review_heatmap/web.py +++ b/src/review_heatmap/web.py @@ -33,13 +33,21 @@ Static web components and templates """ -from __future__ import (absolute_import, division, - print_function, unicode_literals) +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) from .libaddon.platform import JSPY_BRIDGE, ANKI20, PLATFORM -__all__ = ["html_main_element", "html_heatmap", - "html_streak", "html_info_nodata"] +__all__ = [ + "html_main_element", + "html_heatmap", + "html_streak", + "html_info_nodata", +] html_main_element = """ @@ -59,7 +67,9 @@
{{content}}
-""".format(bridge=JSPY_BRIDGE, anki21=not ANKI20, platform=PLATFORM) +""".format( + bridge=JSPY_BRIDGE, anki21=not ANKI20, platform=PLATFORM +) html_heatmap = """
@@ -109,6 +119,12 @@ Current streak: {text_streak_cur} + Total time spent: + {text_time_spent_max} + Daily time average: + {text_time_spent_daily_avg}
"""