Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 55 additions & 52 deletions src/review_heatmap/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -50,7 +54,6 @@


class ActivityReporter(object):

def __init__(self, col, config, whole=False):
self.col = col
self.config = config
Expand All @@ -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
#########################################################################
Expand All @@ -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
Expand All @@ -106,6 +108,7 @@ def _getActivity(self, history, forecast={}):
current = 0

total += activity
total_time_spent += time_spent

days_learned = idx + 1

Expand All @@ -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
#
Expand All @@ -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]

Expand All @@ -143,32 +148,24 @@ 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

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
Expand Down Expand Up @@ -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):
Expand All @@ -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)

Expand All @@ -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:
Expand Down Expand Up @@ -270,16 +264,15 @@ 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"]
if self.whole:
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)
Expand All @@ -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
#########################################################################
Expand All @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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)
31 changes: 28 additions & 3 deletions src/review_heatmap/heatmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,37 @@ 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)),
)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like there's a closing ) missing here, which currently causes:

When loading '⁨review_heatmap⁩':
⁨Traceback (most recent call last):
  File "/home/ari/Projects/AnkiDesktop21/qt/aqt/addons.py", line 208, in loadAddons
    __import__(addon.dir_name)
  File "/home/ari/Tests/Anki/review-heatmap/addons21/review_heatmap/__init__.py", line 118, in <module>
    initializeAddon()
  File "/home/ari/Tests/Anki/review-heatmap/addons21/review_heatmap/__init__.py", line 109, in initializeAddon
    from .views import initializeViews
  File "/home/ari/Tests/Anki/review-heatmap/addons21/review_heatmap/views.py", line 53, in <module>
    from .heatmap import HeatmapCreator
  File "/home/ari/Tests/Anki/review-heatmap/addons21/review_heatmap/heatmap.py", line 85
    }
    ^
SyntaxError: closing parenthesis '}' does not match opening parenthesis '(' on line 79

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry about that. I had customized an earlier version of the add-on, and had to deal with a number of conflicts. Didn't properly test this version.


}

legend_factors = (0.125, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 4)

stat_units = {
"streak": "day",
"percentage": None,
"cards": "card"
"cards": "card",
"time_day": "hours",
"time_minute": "minutes",
}

def __init__(self, config, whole=False):
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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 ""
Expand Down
26 changes: 21 additions & 5 deletions src/review_heatmap/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<script type="text/javascript" src="qrc:/review_heatmap/web/d3.min.js"></script>
Expand All @@ -59,7 +67,9 @@
<div class="rh-container {{classes}}">
{{content}}
</div>
""".format(bridge=JSPY_BRIDGE, anki21=not ANKI20, platform=PLATFORM)
""".format(
bridge=JSPY_BRIDGE, anki21=not ANKI20, platform=PLATFORM
)

html_heatmap = """
<div class="heatmap">
Expand Down Expand Up @@ -109,6 +119,12 @@
<span class="streak-info">Current streak:</span>
<span title="Current card review activity streak. All types of repetitions included."
class="sstats {class_streak_cur}">{text_streak_cur}</span>
<span class="streak-info">Total time spent:</span>
<span title="Total time spent reviewing cards"
class="sstats {class_time_spent_max}">{text_time_spent_max}</span>
<span class="streak-info">Daily time average:</span>
<span title="Daily average of time spent reviewing cards"
class="sstats {class_time_spent_daily_avg}">{text_time_spent_daily_avg}</span>
</div>
"""

Expand Down