# -*- coding: utf-8 -*- # Copyright: Damien Elmes # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html import os, sys, time, datetime, simplejson from anki.lang import _ import anki.js colYoung = "#7c7" colMature = "#070" colCum = "rgba(0,0,0,0.9)" colLearn = "#00F" colRelearn = "#c00" colCram = "#ff0" colIvl = "#077" easesNewC = "#80b3ff" easesYoungC = "#5555ff" easesMatureC = "#0f5aff" class Graphs(object): def __init__(self, deck, selective=True): self.deck = deck self._stats = None self.selective = selective def report(self): txt = (self.dueGraph(0, 30, _("Due/Day")) + self.dueGraph(0, 52, _("Due/Week"), chunk=7) + self.dueGraph(0, None, _("Due/Month"), chunk=30) + self.repsGraph(30, _("Reviewed/Day"), _("Time/Day")) + self.repsGraph(52, _("Reviewed/Week"), _("Time/Week"), chunk=7) + self.repsGraph(None, _("Reviewed/Month"), _("Time/Month"), chunk=30) + self.ivlGraph() + self.easeGraph()) return "
%s
" % (anki.js.all, txt) # Due and cumulative due ###################################################################### def dueGraph(self, start, end, title, chunk=1): d = self._due(start, end, chunk) yng = [] mtr = [] tot = 0 totd = [] for day in d: yng.append((day[0], day[1])) mtr.append((day[0], day[2])) tot += day[1]+day[2] totd.append((day[0], tot)) txt = self._graph(id=hash(title), title=title, data=[ dict(data=mtr, color=colMature, label=_("Mature")), dict(data=yng, color=colYoung, label=_("Young")), dict(data=totd, color=colCum, label=_("Cumulative"), yaxis=2, bars={'show': False}, lines=dict(show=True), stack=False) ], conf=dict( xaxis=dict(tickDecimals=0), yaxes=[dict(), dict(position="right")])) return txt def _due(self, start=None, end=None, chunk=1): lim = "" if start is not None: lim += " and due-:today >= %d" % start if end is not None: lim += " and day < %d" % end return self.deck.db.all(""" select (due-:today+1)/:chunk as day, sum(case when ivl < 21 then 1 else 0 end), -- yng sum(case when ivl >= 21 then 1 else 0 end) -- mtr from cards where queue = 2 %s %s group by day order by day""" % (self._limit(), lim), today=self.deck.sched.today, chunk=chunk) # Reps and time spent ###################################################################### def repsGraph(self, days, reptitle, timetitle, chunk=1): d = self._done(days, chunk) # reps lrn = [] yng = [] mtr = [] lapse = [] cram = [] tot = 0 totd = [] for row in d: lrn.append((row[0], row[1])) yng.append((row[0], row[2])) mtr.append((row[0], row[3])) lapse.append((row[0], row[4])) cram.append((row[0], row[5])) tot += row[1]+row[2]+row[3]+row[4]+row[5] totd.append((row[0], tot)) conf = dict( xaxis=dict(tickDecimals=0), yaxes=[dict(), dict(position="right")]) if days is not None: conf['xaxis']['min'] = -days def plot(title): return self._graph("g%d"%hash(title), title=title, data=[ dict(data=mtr, color=colMature, label=_("Mature")), dict(data=yng, color=colYoung, label=_("Young")), dict(data=lapse, color=colRelearn, label=_("Relearning")), dict(data=lrn, color=colLearn, label=_("Learning")), dict(data=cram, color=colCram, label=_("Cramming")), dict(data=totd, color=colCum, label=_("Cumulative"), yaxis=2, bars={'show': False}, lines=dict(show=True), stack=False) ], conf=conf) txt = plot(reptitle) # time lrn = [] yng = [] mtr = [] lapse = [] cram = [] tot = 0 totd = [] for row in d: lrn.append((row[0], row[6])) yng.append((row[0], row[7])) mtr.append((row[0], row[8])) lapse.append((row[0], row[9])) cram.append((row[0], row[10])) tot += row[6]+row[7]+row[8]+row[9]+row[10] totd.append((row[0], tot)) txt += plot(timetitle) return txt def _done(self, num=7, chunk=1): # without selective for now lim = "" if num is not None: lim += "where time > %d" % ( (self.deck.sched.dayCutoff-(num*chunk*86400))*1000) return self.deck.db.all(""" select (cast((time/1000 - :cut) / 86400.0 as int)+1)/:chunk as day, sum(case when type = 0 then 1 else 0 end), -- lrn count sum(case when type = 1 and lastIvl < 21 then 1 else 0 end), -- yng count sum(case when type = 1 and lastIvl >= 21 then 1 else 0 end), -- mtr count sum(case when type = 2 then 1 else 0 end), -- lapse count sum(case when type = 3 then 1 else 0 end), -- cram count sum(case when type = 0 then taken/1000 else 0 end)/3600.0, -- lrn time -- yng + mtr time sum(case when type = 1 and lastIvl < 21 then taken/1000 else 0 end)/3600.0, sum(case when type = 1 and lastIvl >= 21 then taken/1000 else 0 end)/3600.0, sum(case when type = 2 then taken/1000 else 0 end)/3600.0, -- lapse time sum(case when type = 3 then taken/1000 else 0 end)/3600.0 -- cram time from revlog %s group by day order by day""" % lim, cut=self.deck.sched.dayCutoff, chunk=chunk) # Intervals ###################################################################### def _ivls(self): return self.deck.db.all(""" select ivl / 7 as grp, count() from cards where queue = 2 %s group by grp order by grp""" % self._limit()) def ivlGraph(self): self._calcStats() ivls = self._stats['ivls'] txt = self._graph(id="ivl", title=_("Intervals"), data=[ dict(data=ivls, color=colIvl) ]) return txt # Eases ###################################################################### def _eases(self): # ignores selective, at least for now return self.deck.db.all(""" select (case when type in (0,2) then 0 when lastIvl < 21 then 1 else 2 end) as thetype, ease, count() from revlog group by thetype, ease order by thetype, ease""") def easeGraph(self): self._calcStats() # 3 + 4 + 4 + spaces on sides and middle = 15 # yng starts at 1+3+1 = 5 # mtr starts at 5+4+1 = 10 d = {'lrn':[], 'yng':[], 'mtr':[]} types = ("lrn", "yng", "mtr") for (type, ease, cnt) in self._stats['eases']: if type == 1: ease += 5 elif type == 2: ease += 10 n = types[type] d[n].append((ease, cnt)) ticks = [[1,1],[2,2],[3,3], [6,1],[7,2],[8,3],[9,4], [11, 1],[12,2],[13,3],[14,4]] txt = self._graph(id="ease", title=_("Eases"), data=[ dict(data=d['lrn'], color=easesNewC, label=_("Learning")), dict(data=d['yng'], color=easesYoungC, label=_("Young")), dict(data=d['mtr'], color=easesMatureC, label=_("Mature")), ], conf=dict( xaxis=dict(ticks=ticks, min=0, max=15))) return txt # Tools ###################################################################### def _calcStats(self): if self._stats: return self._stats = {} self._stats['ivls'] = self._ivls() self._stats['done'] = self._done() self._stats['eases'] = self._eases() def _graph(self, id, title, data, conf={}, width=600, height=200, type="bars"): # display settings conf['legend'] = {'container': "#%sLegend" % id} conf['series'] = dict(stack=True) if type == "bars": conf['series']['bars'] = dict( show=True, barWidth=0.8, align="center", fill=0.7, lineWidth=0) elif type == "fill": conf['series']['lines'] = dict(show=True, fill=True) return ( """

%(title)s

Cards

""" % dict( id=id, title=title, w=width, h=height, tw=width+100, data=simplejson.dumps(data), conf=simplejson.dumps(conf))) def _limit(self): if self.selective: return self.deck.sched._groupLimit("rev") else: return ""