diff --git a/anki/stats.py b/anki/stats.py
index 8162eba4d..12aa9eae8 100644
--- a/anki/stats.py
+++ b/anki/stats.py
@@ -70,6 +70,7 @@ colLearn = "#00F"
colRelearn = "#c00"
colCram = "#ff0"
colIvl = "#077"
+colHour = "#777"
colTime = "#770"
colUnseen = "#000"
colSusp = "#ff0"
@@ -93,6 +94,7 @@ class DeckStats(object):
txt += self.repsGraph()
txt += self.ivlGraph()
# other graphs
+ txt += self.hourGraph()
txt += self.easeGraph()
txt += self.cardGraph()
return "
%s" % (anki.js.all, txt)
@@ -446,6 +448,57 @@ ease, count() from revlog %s
group by thetype, ease
order by thetype, ease""" % lim)
+ # Hourly retention
+ ######################################################################
+
+ def hourGraph(self):
+ data = self._hourRet()
+ if not data:
+ return ""
+ shifted = []
+ trend = []
+ for d in data:
+ hour = (d[0] - 4) % 24
+ pct = d[1]
+ shifted.append((hour, pct))
+ shifted.sort()
+ for d in shifted:
+ hour = d[0]
+ pct = d[1]
+ if not trend:
+ trend.append((hour, pct))
+ else:
+ prev = trend[-1][1]
+ diff = pct-prev
+ diff /= 3.0
+ diff = round(diff, 1)
+ trend.append((hour, prev+diff))
+ txt = self._title(_("Retention by hour"),
+ _("Percentage of failures in each hour of the day."))
+ txt += self._graph(id="hour", data=[
+ dict(data=shifted, color=colHour, label=_("% Failed")),
+ dict(data=trend, color=colCum, label=_("Trend"),
+ bars={'show': False}, lines=dict(show=True), stack=False)
+ ], conf=dict(
+ xaxis=dict(ticks=[[0, _("4AM")], [6, _("10AM")],
+ [12, _("4PM")], [18, _("10PM")], [23, _("3AM")]])),
+ ylabel=_("Failure%"))
+ return txt
+
+ def _hourRet(self):
+ lim = self._revlogLimit()
+ if lim:
+ lim = " and " + lim
+ sd = datetime.datetime.fromtimestamp(self.deck.crt)
+ return self.deck.db.all("""
+select
+23 - ((cast((:cut - time/1000) / 3600.0 as int)) %% 24) as hour,
+sum(case when ease = 1 then 1 else 0 end) /
+cast(count() as float) * 100
+from revlog where type = 1 %s
+group by hour order by hour""" % lim,
+ cut=self.deck.sched.dayCutoff-(sd.hour*3600))
+
# Cards
######################################################################