anki/anki/stats.py

927 lines
32 KiB
Python

# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import division
import time
import datetime
import json
import anki.js
from anki.utils import fmtTimeSpan, ids2str
from anki.lang import _, ngettext
# Card stats
##########################################################################
class CardStats(object):
def __init__(self, col, card):
self.col = col
self.card = card
def report(self):
c = self.card
fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs)
self.txt = "<table width=100%>"
self.addLine(_("Added"), self.date(c.id/1000))
first = self.col.db.scalar(
"select min(id) from revlog where cid = ?", c.id)
last = self.col.db.scalar(
"select max(id) from revlog where cid = ?", c.id)
if first:
self.addLine(_("First Review"), self.date(first/1000))
self.addLine(_("Latest Review"), self.date(last/1000))
if c.type in (1,2):
if c.odid or c.queue < 0:
next = None
else:
if c.queue in (2,3):
next = time.time()+((c.due - self.col.sched.today)*86400)
else:
next = c.due
next = self.date(next)
if next:
self.addLine(_("Due"), next)
if c.queue == 2:
self.addLine(_("Interval"), fmt(c.ivl * 86400))
self.addLine(_("Ease"), "%d%%" % (c.factor/10.0))
self.addLine(_("Reviews"), "%d" % c.reps)
self.addLine(_("Lapses"), "%d" % c.lapses)
(cnt, total) = self.col.db.first(
"select count(), sum(time)/1000 from revlog where cid = :id",
id=c.id)
if cnt:
self.addLine(_("Average Time"), self.time(total / float(cnt)))
self.addLine(_("Total Time"), self.time(total))
elif c.queue == 0:
self.addLine(_("Position"), c.due)
self.addLine(_("Card Type"), c.template()['name'])
self.addLine(_("Note Type"), c.model()['name'])
self.addLine(_("Deck"), self.col.decks.name(c.did))
self.addLine(_("Note ID"), c.nid)
self.addLine(_("Card ID"), c.id)
self.txt += "</table>"
return self.txt
def addLine(self, k, v):
self.txt += self.makeLine(k, v)
def makeLine(self, k, v):
txt = "<tr><td align=left style='padding-right: 3px;'>"
txt += "<b>%s</b></td><td>%s</td></tr>" % (k, v)
return txt
def date(self, tm):
return time.strftime("%Y-%m-%d", time.localtime(tm))
def time(self, tm):
str = ""
if tm >= 60:
str = fmtTimeSpan((tm/60)*60, short=True, point=-1, unit=1)
if tm%60 != 0 or not str:
str += fmtTimeSpan(tm%60, point=2 if not str else -1, short=True)
return str
# Collection stats
##########################################################################
colYoung = "#7c7"
colMature = "#070"
colCum = "rgba(0,0,0,0.9)"
colLearn = "#00F"
colRelearn = "#c00"
colCram = "#ff0"
colIvl = "#077"
colHour = "#ccc"
colTime = "#770"
colUnseen = "#000"
colSusp = "#ff0"
class CollectionStats(object):
def __init__(self, col):
self.col = col
self._stats = None
self.type = 0
self.width = 600
self.height = 200
self.wholeCollection = False
def report(self, type=0):
# 0=days, 1=weeks, 2=months
self.type = type
from statsbg import bg
txt = self.css % bg
txt += self.todayStats()
txt += self.dueGraph()
txt += self.repsGraph()
txt += self.introductionGraph()
txt += self.ivlGraph()
txt += self.hourGraph()
txt += self.easeGraph()
txt += self.cardGraph()
txt += self.footer()
return "<script>%s\n</script><center>%s</center>" % (
anki.js.jquery+anki.js.plot, txt)
css = """
<style>
h1 { margin-bottom: 0; margin-top: 1em; }
.pielabel { text-align:center; padding:0px; color:white; }
body {background-image: url(data:image/png;base64,%s); }
</style>
"""
# Today stats
######################################################################
def todayStats(self):
b = self._title(_("Today"))
# studied today
lim = self._revlogLimit()
if lim:
lim = " and " + lim
cards, thetime, failed, lrn, rev, relrn, filt = self.col.db.first("""
select count(), sum(time)/1000,
sum(case when ease = 1 then 1 else 0 end), /* failed */
sum(case when type = 0 then 1 else 0 end), /* learning */
sum(case when type = 1 then 1 else 0 end), /* review */
sum(case when type = 2 then 1 else 0 end), /* relearn */
sum(case when type = 3 then 1 else 0 end) /* filter */
from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000)
cards = cards or 0
thetime = thetime or 0
failed = failed or 0
lrn = lrn or 0
rev = rev or 0
relrn = relrn or 0
filt = filt or 0
# studied
def bold(s):
return "<b>"+unicode(s)+"</b>"
msgp1 = ngettext("<!--studied-->%d card", "<!--studied-->%d cards", cards) % cards
b += _("Studied %(a)s in %(b)s today.") % dict(
a=bold(msgp1), b=bold(fmtTimeSpan(thetime, unit=1)))
# again/pass count
b += "<br>" + _("Again count: %s") % bold(failed)
if cards:
b += " " + _("(%s correct)") % bold(
"%0.1f%%" %((1-failed/float(cards))*100))
# type breakdown
b += "<br>"
b += (_("Learn: %(a)s, Review: %(b)s, Relearn: %(c)s, Filtered: %(d)s")
% dict(a=bold(lrn), b=bold(rev), c=bold(relrn), d=bold(filt)))
# mature today
mcnt, msum = self.col.db.first("""
select count(), sum(case when ease = 1 then 0 else 1 end) from revlog
where lastIvl >= 21 and id > ?"""+lim, (self.col.sched.dayCutoff-86400)*1000)
b += "<br>"
if mcnt:
b += _("Correct answers on mature cards: %(a)d/%(b)d (%(c).1f%%)") % dict(
a=msum, b=mcnt, c=(msum / float(mcnt) * 100))
else:
b += _("No mature cards were studied today.")
return b
# Due and cumulative due
######################################################################
def dueGraph(self):
if self.type == 0:
start = 0; end = 31; chunk = 1;
elif self.type == 1:
start = 0; end = 52; chunk = 7
elif self.type == 2:
start = 0; end = None; chunk = 30
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))
data = [
dict(data=mtr, color=colMature, label=_("Mature")),
dict(data=yng, color=colYoung, label=_("Young")),
]
if len(totd) > 1:
data.append(
dict(data=totd, color=colCum, label=_("Cumulative"), yaxis=2,
bars={'show': False}, lines=dict(show=True), stack=False))
txt = self._title(
_("Forecast"),
_("The number of reviews due in the future."))
xaxis = dict(tickDecimals=0, min=-0.5)
if end is not None:
xaxis['max'] = end-0.5
txt += self._graph(id="due", data=data,
ylabel2=_("Cumulative Cards"), conf=dict(
xaxis=xaxis, yaxes=[dict(min=0), dict(
min=0, tickDecimals=0, position="right")]))
txt += self._dueInfo(tot, len(totd)*chunk)
return txt
def _dueInfo(self, tot, num):
i = []
self._line(i, _("Total"), ngettext("%d review", "%d reviews", tot) % tot)
self._line(i, _("Average"), self._avgDay(
tot, num, _("reviews")))
tomorrow = self.col.db.scalar("""
select count() from cards where did in %s and queue in (2,3)
and due = ?""" % self._limit(), self.col.sched.today+1)
tomorrow = ngettext("%d card", "%d cards", tomorrow) % tomorrow
self._line(i, _("Due tomorrow"), tomorrow)
return self._lineTbl(i)
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.col.db.all("""
select (due-:today)/: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 did in %s and queue in (2,3)
%s
group by day order by day""" % (self._limit(), lim),
today=self.col.sched.today,
chunk=chunk)
# Added, reps and time spent
######################################################################
def introductionGraph(self):
if self.type == 0:
days = 30; chunk = 1
elif self.type == 1:
days = 52; chunk = 7
else:
days = None; chunk = 30
return self._introductionGraph(self._added(days, chunk),
days, _("Added"))
def _introductionGraph(self, data, days, title):
if not data:
return ""
d = data
conf = dict(
xaxis=dict(tickDecimals=0, max=0.5),
yaxes=[dict(min=0), dict(position="right",min=0)])
if days is not None:
conf['xaxis']['min'] = -days+0.5
def plot(id, data, ylabel, ylabel2):
return self._graph(
id, data=data, conf=conf, ylabel=ylabel, ylabel2=ylabel2)
# graph
(repdata, repsum) = self._splitRepData(d, ((1, colLearn, ""),))
txt = self._title(
title, _("The number of new cards you have added."))
txt += plot("intro", repdata, ylabel=_("Cards"), ylabel2=_("Cumulative Cards"))
# total and per day average
tot = sum([i[1] for i in d])
period = self._periodDays()
if not period:
# base off date of earliest added card
period = self._deckAge('add')
i = []
self._line(i, _("Total"), ngettext("%d card", "%d cards", tot) % tot)
self._line(i, _("Average"), self._avgDay(tot, period, _("cards")))
txt += self._lineTbl(i)
return txt
def repsGraph(self):
if self.type == 0:
days = 30; chunk = 1
elif self.type == 1:
days = 52; chunk = 7
else:
days = None; chunk = 30
return self._repsGraph(self._done(days, chunk),
days,
_("Review Count"),
_("Review Time"))
def _repsGraph(self, data, days, reptitle, timetitle):
if not data:
return ""
d = data
conf = dict(
xaxis=dict(tickDecimals=0, max=0.5),
yaxes=[dict(min=0), dict(position="right",min=0)])
if days is not None:
conf['xaxis']['min'] = -days+0.5
def plot(id, data, ylabel, ylabel2):
return self._graph(
id, data=data, conf=conf, ylabel=ylabel, ylabel2=ylabel2)
# reps
(repdata, repsum) = self._splitRepData(d, (
(3, colMature, _("Mature")),
(2, colYoung, _("Young")),
(4, colRelearn, _("Relearn")),
(1, colLearn, _("Learn")),
(5, colCram, _("Cram"))))
txt = self._title(
reptitle, _("The number of questions you have answered."))
txt += plot("reps", repdata, ylabel=_("Answers"), ylabel2=_(
"Cumulative Answers"))
(daysStud, fstDay) = self._daysStudied()
rep, tot = self._ansInfo(repsum, daysStud, fstDay, _("reviews"))
txt += rep
# time
(timdata, timsum) = self._splitRepData(d, (
(8, colMature, _("Mature")),
(7, colYoung, _("Young")),
(9, colRelearn, _("Relearn")),
(6, colLearn, _("Learn")),
(10, colCram, _("Cram"))))
if self.type == 0:
t = _("Minutes")
convHours = False
else:
t = _("Hours")
convHours = True
txt += self._title(timetitle, _("The time taken to answer the questions."))
txt += plot("time", timdata, ylabel=t, ylabel2=_("Cumulative %s") % t)
rep, tot2 = self._ansInfo(
timsum, daysStud, fstDay, _("minutes"), convHours, total=tot)
txt += rep
return txt
def _ansInfo(self, totd, studied, first, unit, convHours=False, total=None):
if not totd:
return
tot = totd[-1][1]
period = self._periodDays()
if not period:
# base off earliest repetition date
period = self._deckAge('review')
i = []
self._line(i, _("Days studied"),
_("<b>%(pct)d%%</b> (%(x)s of %(y)s)") % dict(
x=studied, y=period, pct=studied/float(period)*100),
bold=False)
if convHours:
tunit = _("hours")
else:
tunit = unit
self._line(i, _("Total"), _("%(tot)s %(unit)s") % dict(
unit=tunit, tot=int(tot)))
if convHours:
# convert to minutes
tot *= 60
self._line(i, _("Average for days studied"), self._avgDay(
tot, studied, unit))
if studied != period:
# don't display if you did study every day
self._line(i, _("If you studied every day"), self._avgDay(
tot, period, unit))
if total and tot:
perMin = total / float(tot)
perMin = round(perMin, 1)
# don't round down to zero
if perMin < 0.1:
text = _("less than 0.1 cards/minute")
else:
text = _("%.01f cards/minute") % perMin
self._line(
i, _("Average answer time"),
_("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=text))
return self._lineTbl(i), int(tot)
def _splitRepData(self, data, spec):
sep = {}
totcnt = {}
totd = {}
alltot = []
allcnt = 0
for (n, col, lab) in spec:
totcnt[n] = 0
totd[n] = []
sum = []
for row in data:
for (n, col, lab) in spec:
if n not in sep:
sep[n] = []
sep[n].append((row[0], row[n]))
totcnt[n] += row[n]
allcnt += row[n]
totd[n].append((row[0], totcnt[n]))
alltot.append((row[0], allcnt))
ret = []
for (n, col, lab) in spec:
if len(totd[n]) and totcnt[n]:
# bars
ret.append(dict(data=sep[n], color=col, label=lab))
# lines
ret.append(dict(
data=totd[n], color=col, label=None, yaxis=2,
bars={'show': False}, lines=dict(show=True), stack=-n))
return (ret, alltot)
def _added(self, num=7, chunk=1):
lims = []
if num is not None:
lims.append("id > %d" % (
(self.col.sched.dayCutoff-(num*chunk*86400))*1000))
lims.append("did in %s" % self._limit())
if lims:
lim = "where " + " and ".join(lims)
else:
lim = ""
if self.type == 0:
tf = 60.0 # minutes
else:
tf = 3600.0 # hours
return self.col.db.all("""
select
(cast((id/1000.0 - :cut) / 86400.0 as int))/:chunk as day,
count(id)
from cards %s
group by day order by day""" % lim, cut=self.col.sched.dayCutoff,tf=tf, chunk=chunk)
def _done(self, num=7, chunk=1):
lims = []
if num is not None:
lims.append("id > %d" % (
(self.col.sched.dayCutoff-(num*chunk*86400))*1000))
lim = self._revlogLimit()
if lim:
lims.append(lim)
if lims:
lim = "where " + " and ".join(lims)
else:
lim = ""
if self.type == 0:
tf = 60.0 # minutes
else:
tf = 3600.0 # hours
return self.col.db.all("""
select
(cast((id/1000.0 - :cut) / 86400.0 as int))/: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 time/1000.0 else 0 end)/:tf, -- lrn time
-- yng + mtr time
sum(case when type = 1 and lastIvl < 21 then time/1000.0 else 0 end)/:tf,
sum(case when type = 1 and lastIvl >= 21 then time/1000.0 else 0 end)/:tf,
sum(case when type = 2 then time/1000.0 else 0 end)/:tf, -- lapse time
sum(case when type = 3 then time/1000.0 else 0 end)/:tf -- cram time
from revlog %s
group by day order by day""" % lim,
cut=self.col.sched.dayCutoff,
tf=tf,
chunk=chunk)
def _daysStudied(self):
lims = []
num = self._periodDays()
if num:
lims.append(
"id > %d" %
((self.col.sched.dayCutoff-(num*86400))*1000))
rlim = self._revlogLimit()
if rlim:
lims.append(rlim)
if lims:
lim = "where " + " and ".join(lims)
else:
lim = ""
return self.col.db.first("""
select count(), abs(min(day)) from (select
(cast((id/1000 - :cut) / 86400.0 as int)+1) as day
from revlog %s
group by day order by day)""" % lim,
cut=self.col.sched.dayCutoff)
# Intervals
######################################################################
def ivlGraph(self):
(ivls, all, avg, max_) = self._ivls()
tot = 0
totd = []
if not ivls or not all:
return ""
for (grp, cnt) in ivls:
tot += cnt
totd.append((grp, tot/float(all)*100))
if self.type == 0:
ivlmax = 31
elif self.type == 1:
ivlmax = 52
else:
ivlmax = max(5, ivls[-1][0])
txt = self._title(_("Intervals"),
_("Delays until reviews are shown again."))
txt += self._graph(id="ivl", ylabel2=_("Percentage"), data=[
dict(data=ivls, color=colIvl),
dict(data=totd, color=colCum, yaxis=2,
bars={'show': False}, lines=dict(show=True), stack=False)
], conf=dict(
xaxis=dict(min=-0.5, max=ivlmax+0.5),
yaxes=[dict(), dict(position="right", max=105)]))
i = []
self._line(i, _("Average interval"), fmtTimeSpan(avg*86400))
self._line(i, _("Longest interval"), fmtTimeSpan(max_*86400))
return txt + self._lineTbl(i)
def _ivls(self):
if self.type == 0:
chunk = 1; lim = " and grp <= 30"
elif self.type == 1:
chunk = 7; lim = " and grp <= 52"
else:
chunk = 30; lim = ""
data = [self.col.db.all("""
select ivl / :chunk as grp, count() from cards
where did in %s and queue = 2 %s
group by grp
order by grp""" % (self._limit(), lim), chunk=chunk)]
return data + list(self.col.db.first("""
select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2""" %
self._limit()))
# Eases
######################################################################
def easeGraph(self):
# 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")
eases = self._eases()
for (type, ease, cnt) in 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._title(_("Answer Buttons"),
_("The number of times you have pressed each button."))
txt += self._graph(id="ease", data=[
dict(data=d['lrn'], color=colLearn, label=_("Learning")),
dict(data=d['yng'], color=colYoung, label=_("Young")),
dict(data=d['mtr'], color=colMature, label=_("Mature")),
], type="barsLine", conf=dict(
xaxis=dict(ticks=ticks, min=0, max=15)),
ylabel=_("Answers"))
txt += self._easeInfo(eases)
return txt
def _easeInfo(self, eases):
types = {0: [0, 0], 1: [0, 0], 2: [0,0]}
for (type, ease, cnt) in eases:
if ease == 1:
types[type][0] += cnt
else:
types[type][1] += cnt
i = []
for type in range(3):
(bad, good) = types[type]
tot = bad + good
try:
pct = good / float(tot) * 100
except:
pct = 0
i.append(_(
"Correct: <b>%(pct)0.2f%%</b><br>(%(good)d of %(tot)d)") % dict(
pct=pct, good=good, tot=tot))
return ("""
<center><table width=%dpx><tr><td width=50></td><td align=center>""" % self.width +
"</td><td align=center>".join(i) +
"</td></tr></table></center>")
def _eases(self):
lims = []
lim = self._revlogLimit()
if lim:
lims.append(lim)
if self.type == 0:
days = 30
elif self.type == 1:
days = 365
else:
days = None
if days is not None:
lims.append("id > %d" % (
(self.col.sched.dayCutoff-(days*86400))*1000))
if lims:
lim = "where " + " and ".join(lims)
else:
lim = ""
return self.col.db.all("""
select (case
when type in (0,2) then 0
when lastIvl < 21 then 1
else 2 end) as thetype,
(case when type in (0,2) and ease = 4 then 3 else ease end), 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 = []
counts = []
mcount = 0
trend = []
peak = 0
for d in data:
hour = (d[0] - 4) % 24
pct = d[1]
if pct > peak:
peak = pct
shifted.append((hour, pct))
counts.append((hour, d[2]))
if d[2] > mcount:
mcount = d[2]
shifted.sort()
counts.sort()
if len(counts) < 4:
return ""
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(_("Hourly Breakdown"),
_("Review success rate for each hour of the day."))
txt += self._graph(id="hour", data=[
dict(data=shifted, color=colCum, label=_("% Correct")),
dict(data=counts, color=colHour, label=_("Answers"), yaxis=2,
bars=dict(barWidth=0.2), stack=False)
], conf=dict(
xaxis=dict(ticks=[[0, _("4AM")], [6, _("10AM")],
[12, _("4PM")], [18, _("10PM")], [23, _("3AM")]]),
yaxes=[dict(max=peak), dict(position="right", max=mcount)]),
ylabel=_("% Correct"), ylabel2=_("Reviews"))
txt += _("Hours with less than 30 reviews are not shown.")
return txt
def _hourRet(self):
lim = self._revlogLimit()
if lim:
lim = " and " + lim
sd = datetime.datetime.fromtimestamp(self.col.crt)
pd = self._periodDays()
if pd:
lim += " and id > %d" % ((self.col.sched.dayCutoff-(86400*pd))*1000)
return self.col.db.all("""
select
23 - ((cast((:cut - id/1000) / 3600.0 as int)) %% 24) as hour,
sum(case when ease = 1 then 0 else 1 end) /
cast(count() as float) * 100,
count()
from revlog where type in (0,1,2) %s
group by hour having count() > 30 order by hour""" % lim,
cut=self.col.sched.dayCutoff-(sd.hour*3600))
# Cards
######################################################################
def cardGraph(self):
# graph data
div = self._cards()
d = []
for c, (t, col) in enumerate((
(_("Mature"), colMature),
(_("Young+Learn"), colYoung),
(_("Unseen"), colUnseen),
(_("Suspended+Buried"), colSusp))):
d.append(dict(data=div[c], label="%s: %s" % (t, div[c]), color=col))
# text data
i = []
(c, f) = self.col.db.first("""
select count(id), count(distinct nid) from cards
where did in %s """ % self._limit())
self._line(i, _("Total cards"), c)
self._line(i, _("Total notes"), f)
(low, avg, high) = self._factors()
if low:
self._line(i, _("Lowest ease"), "%d%%" % low)
self._line(i, _("Average ease"), "%d%%" % avg)
self._line(i, _("Highest ease"), "%d%%" % high)
info = "<table width=100%>" + "".join(i) + "</table><p>"
info += _('''\
A card's <i>ease</i> is the size of the next interval \
when you answer "good" on a review.''')
txt = self._title(_("Cards Types"),
_("The division of cards in your deck(s)."))
txt += "<table width=%d><tr><td>%s</td><td>%s</td></table>" % (
self.width,
self._graph(id="cards", data=d, type="pie"),
info)
return txt
def _line(self, i, a, b, bold=True):
colon = _(":")
if bold:
i.append(("<tr><td width=200 align=right>%s%s</td><td><b>%s</b></td></tr>") % (a,colon,b))
else:
i.append(("<tr><td width=200 align=right>%s%s</td><td>%s</td></tr>") % (a,colon,b))
def _lineTbl(self, i):
return "<table width=400>" + "".join(i) + "</table>"
def _factors(self):
return self.col.db.first("""
select
min(factor) / 10.0,
avg(factor) / 10.0,
max(factor) / 10.0
from cards where did in %s and queue = 2""" % self._limit())
def _cards(self):
return self.col.db.first("""
select
sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr
sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn
sum(case when queue=0 then 1 else 0 end), -- new
sum(case when queue<0 then 1 else 0 end) -- susp
from cards where did in %s""" % self._limit())
# Footer
######################################################################
def footer(self):
b = "<br><br><font size=1>"
b += _("Generated on %s") % time.asctime(time.localtime(time.time()))
b += "<br>"
if self.wholeCollection:
deck = _("whole collection")
else:
deck = self.col.decks.current()['name']
b += _("Scope: %s") % deck
b += "<br>"
b += _("Period: %s") % [
_("1 month"),
_("1 year"),
_("deck life")
][self.type]
return b
# Tools
######################################################################
def _graph(self, id, data, conf={},
type="bars", ylabel=_("Cards"), timeTicks=True, ylabel2=""):
# display settings
if type == "pie":
conf['legend'] = {'container': "#%sLegend" % id, 'noColumns':2}
else:
conf['legend'] = {'container': "#%sLegend" % id, 'noColumns':10}
conf['series'] = dict(stack=True)
if not 'yaxis' in conf:
conf['yaxis'] = {}
conf['yaxis']['labelWidth'] = 40
if 'xaxis' not in conf:
conf['xaxis'] = {}
if timeTicks:
conf['timeTicks'] = (_("d"), _("w"), _("mo"))[self.type]
# types
width = self.width
height = self.height
if type == "bars":
conf['series']['bars'] = dict(
show=True, barWidth=0.8, align="center", fill=0.7, lineWidth=0)
elif type == "barsLine":
conf['series']['bars'] = dict(
show=True, barWidth=0.8, align="center", fill=0.7, lineWidth=3)
elif type == "fill":
conf['series']['lines'] = dict(show=True, fill=True)
elif type == "pie":
width /= 2.3
height *= 1.5
ylabel = ""
conf['series']['pie'] = dict(
show=True,
radius=1,
stroke=dict(color="#fff", width=5),
label=dict(
show=True,
radius=0.8,
threshold=0.01,
background=dict(
opacity=0.5,
color="#000"
)))
#conf['legend'] = dict(show=False)
return (
"""
<table cellpadding=0 cellspacing=10>
<tr>
<td><div style="width: 150px; text-align: center; position:absolute;
-webkit-transform: rotate(-90deg) translateY(-85px);
font-weight: bold;
">%(ylab)s</div></td>
<td>
<center><div id=%(id)sLegend></div></center>
<div id="%(id)s" style="width:%(w)spx; height:%(h)spx;"></div>
</td>
<td><div style="width: 150px; text-align: center; position:absolute;
-webkit-transform: rotate(90deg) translateY(65px);
font-weight: bold;
">%(ylab2)s</div></td>
</tr></table>
<script>
$(function () {
var conf = %(conf)s;
if (conf.timeTicks) {
conf.xaxis.tickFormatter = function (val, axis) {
return val.toFixed(0)+conf.timeTicks;
}
}
conf.yaxis.minTickSize = 1;
conf.yaxis.tickFormatter = function (val, axis) {
return val.toFixed(0);
}
if (conf.series.pie) {
conf.series.pie.label.formatter = function(label, series){
return '<div class=pielabel>'+Math.round(series.percent)+'%%</div>';
};
}
$.plot($("#%(id)s"), %(data)s, conf);
});
</script>""" % dict(
id=id, w=width, h=height,
ylab=ylabel, ylab2=ylabel2,
data=json.dumps(data), conf=json.dumps(conf)))
def _limit(self):
if self.wholeCollection:
return ids2str([d['id'] for d in self.col.decks.all()])
return self.col.sched._deckLimit()
def _revlogLimit(self):
if self.wholeCollection:
return ""
return ("cid in (select id from cards where did in %s)" %
ids2str(self.col.decks.active()))
def _title(self, title, subtitle=""):
return '<h1>%s</h1>%s' % (title, subtitle)
def _deckAge(self, by):
lim = self._revlogLimit()
if lim:
lim = " where " + lim
if by == 'review':
t = self.col.db.scalar("select id from revlog %s order by id limit 1" % lim)
elif by == 'add':
lim = "where did in %s" % ids2str(self.col.decks.active())
t = self.col.db.scalar("select id from cards %s order by id limit 1" % lim)
if not t:
period = 1
else:
period = max(
1, int(1+((self.col.sched.dayCutoff - (t/1000)) / 86400)))
return period
def _periodDays(self):
if self.type == 0:
return 30
elif self.type == 1:
return 365
else:
return None
def _avgDay(self, tot, num, unit):
vals = []
try:
vals.append(_("%(a)0.1f %(b)s/day") % dict(a=tot/float(num), b=unit))
return ", ".join(vals)
except ZeroDivisionError:
return ""