mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 22:42:25 -04:00
eases graph
This commit is contained in:
parent
f850ce8b06
commit
60ef1ec49f
2 changed files with 134 additions and 241 deletions
369
anki/graphs.py
369
anki/graphs.py
|
@ -27,171 +27,54 @@ class Graphs(object):
|
||||||
self._stats = None
|
self._stats = None
|
||||||
self.selective = selective
|
self.selective = selective
|
||||||
|
|
||||||
def workDone(self, days=30):
|
# Due and cumulative due
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
def dueGraph(self):
|
||||||
self._calcStats()
|
self._calcStats()
|
||||||
|
d = self._stats['due']
|
||||||
|
yng = []
|
||||||
|
mtr = []
|
||||||
|
for day in d:
|
||||||
|
yng.append((day[0], day[1]))
|
||||||
|
mtr.append((day[0], day[2]))
|
||||||
|
txt = self._graph(id="due", data=[
|
||||||
|
dict(data=yng, bars=dict(show=True, barWidth=0.8),
|
||||||
|
color=dueYoungC, label=_("Young")),
|
||||||
|
dict(data=mtr, bars=dict(show=True, barWidth=0.8),
|
||||||
|
color=dueMatureC, label=_("Mature"))
|
||||||
|
])
|
||||||
|
return txt
|
||||||
|
|
||||||
for type in ["dayRepsNew", "dayRepsYoung", "dayRepsMature"]:
|
def cumDueGraph(self):
|
||||||
self._addMissing(self.stats[type], -days, 0)
|
|
||||||
|
|
||||||
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
|
|
||||||
graph = fig.add_subplot(111)
|
|
||||||
|
|
||||||
args = sum((self._unzip(self.stats[type].items(), limit=days, reverseLimit=True) for type in ["dayRepsMature", "dayRepsYoung", "dayRepsNew"][::-1]), [])
|
|
||||||
|
|
||||||
self._varGraph(graph, days, [reviewNewC, reviewYoungC, reviewMatureC], *args)
|
|
||||||
|
|
||||||
cheat = fig.add_subplot(111)
|
|
||||||
b1 = cheat.bar(-3, 0, color = reviewNewC)
|
|
||||||
b2 = cheat.bar(-4, 0, color = reviewYoungC)
|
|
||||||
b3 = cheat.bar(-5, 0, color = reviewMatureC)
|
|
||||||
|
|
||||||
cheat.legend([b1, b2, b3], [
|
|
||||||
"New",
|
|
||||||
"Young",
|
|
||||||
"Mature"], loc='upper left')
|
|
||||||
|
|
||||||
graph.set_xlim(xmin=-days+1, xmax=1)
|
|
||||||
graph.set_ylim(ymax=max(max(a for a in args[1::2])) + 10)
|
|
||||||
graph.set_xlabel("Day (0 = today)")
|
|
||||||
graph.set_ylabel("Cards Answered")
|
|
||||||
|
|
||||||
return fig
|
|
||||||
|
|
||||||
def timeSpent(self, days=30):
|
|
||||||
self._calcStats()
|
self._calcStats()
|
||||||
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
|
d = self._stats['due']
|
||||||
times = self.stats['dayTimes']
|
tot = 0
|
||||||
self._addMissing(times, -days+1, 0)
|
days = []
|
||||||
times = self._unzip([(day,y) for (day,y) in times.items()
|
for day in d:
|
||||||
if day + days >= 0])
|
tot += day[1]+day[2]
|
||||||
graph = fig.add_subplot(111)
|
days.append((day[0], tot))
|
||||||
self._varGraph(graph, days, reviewTimeC, *times)
|
txt = self._graph(id="cum", data=[
|
||||||
graph.set_xlim(xmin=-days+1, xmax=1)
|
dict(data=days, lines=dict(show=True, fill=True),
|
||||||
graph.set_ylim(ymax=max(a for a in times[1]) + 0.1)
|
color=dueCumulC, label=_("Cards")),
|
||||||
graph.set_xlabel("Day (0 = today)")
|
])
|
||||||
graph.set_ylabel("Minutes")
|
return txt
|
||||||
return fig
|
|
||||||
|
|
||||||
def addedRecently(self, numdays=30, attr='crt'):
|
def _due(self, days=7):
|
||||||
self._calcStats()
|
return self.deck.db.all("""
|
||||||
days = {}
|
select due-:today,
|
||||||
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
|
count(), -- all
|
||||||
limit = self.endOfDay - (numdays) * 86400
|
sum(case when ivl >= 21 then 1 else 0 end) -- mature
|
||||||
if attr == "created":
|
from cards
|
||||||
res = self.deck.db.list("select %s from cards where %s >= %f" %
|
where queue = 2 and due < (:today+:days) %s
|
||||||
(attr, attr, limit))
|
group by due order by due""" % self._limit(),
|
||||||
else:
|
today=self.deck.sched.today, days=days)
|
||||||
# firstAnswered
|
|
||||||
res = self.deck.db.list(
|
|
||||||
"select time/1000 from revlog where rep = 1")
|
|
||||||
for r in res:
|
|
||||||
d = int((r - self.endOfDay) / 86400.0)
|
|
||||||
days[d] = days.get(d, 0) + 1
|
|
||||||
self._addMissing(days, -numdays+1, 0)
|
|
||||||
graph = fig.add_subplot(111)
|
|
||||||
ivls = self._unzip(days.items())
|
|
||||||
if attr == 'created':
|
|
||||||
colour = addedC
|
|
||||||
else:
|
|
||||||
colour = firstC
|
|
||||||
self._varGraph(graph, numdays, colour, *ivls)
|
|
||||||
graph.set_xlim(xmin=-numdays+1, xmax=1)
|
|
||||||
graph.set_xlabel("Day (0 = today)")
|
|
||||||
if attr == 'created':
|
|
||||||
graph.set_ylabel("Cards Added")
|
|
||||||
else:
|
|
||||||
graph.set_ylabel("Cards First Answered")
|
|
||||||
return fig
|
|
||||||
|
|
||||||
def easeBars(self):
|
# Reps and time spent
|
||||||
fig = Figure(figsize=(3, 3), dpi=self.dpi)
|
######################################################################
|
||||||
graph = fig.add_subplot(111)
|
|
||||||
types = ("new", "young", "mature")
|
|
||||||
enum = 5
|
|
||||||
offset = 0
|
|
||||||
arrsize = 16
|
|
||||||
arr = [0] * arrsize
|
|
||||||
colours = [easesNewC, easesYoungC, easesMatureC]
|
|
||||||
bars = []
|
|
||||||
eases = self.deck.db.all("""
|
|
||||||
select (case when rep = 1 then 0 when lastIvl <= 21 then 1 else 2 end)
|
|
||||||
as type, ease, count() from revlog group by type, ease""")
|
|
||||||
if not eases:
|
|
||||||
return None
|
|
||||||
d = {}
|
|
||||||
for (type, ease, count) in eases:
|
|
||||||
type = types[type]
|
|
||||||
if type not in d:
|
|
||||||
d[type] = {}
|
|
||||||
d[type][ease] = count
|
|
||||||
for n, type in enumerate(types):
|
|
||||||
total = float(sum(d[type].values()))
|
|
||||||
for e in range(1, enum):
|
|
||||||
try:
|
|
||||||
arr[e+offset] = (d[type][e] / total) * 100 + 1
|
|
||||||
except ZeroDivisionError:
|
|
||||||
arr[e+offset] = 0
|
|
||||||
bars.append(graph.bar(range(arrsize), arr, width=1.0,
|
|
||||||
color=colours[n], align='center'))
|
|
||||||
arr = [0] * arrsize
|
|
||||||
offset += 5
|
|
||||||
x = ([""] + [str(n) for n in range(1, enum)]) * 3
|
|
||||||
graph.legend([p[0] for p in bars], ("New",
|
|
||||||
"Young",
|
|
||||||
"Mature"),
|
|
||||||
'upper left')
|
|
||||||
graph.set_ylim(ymax=100)
|
|
||||||
graph.set_xlim(xmax=15)
|
|
||||||
graph.set_xticks(range(arrsize))
|
|
||||||
graph.set_xticklabels(x)
|
|
||||||
graph.set_ylabel("% of Answers")
|
|
||||||
graph.set_xlabel("Answer Buttons")
|
|
||||||
graph.grid(True)
|
|
||||||
return fig
|
|
||||||
|
|
||||||
def _calcStats(self):
|
def _done(self):
|
||||||
if self._stats:
|
|
||||||
return
|
|
||||||
self._stats = {}
|
|
||||||
self._stats['due'] = self._dueCards()
|
|
||||||
self._stats['ivls'] = self._ivls()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
days = {}
|
|
||||||
daysYoung = {}
|
|
||||||
daysMature = {}
|
|
||||||
months = {}
|
|
||||||
next = {}
|
|
||||||
lowestInDay = 0
|
|
||||||
self.endOfDay = self.deck.sched.dayCutoff
|
|
||||||
t = time.time()
|
|
||||||
young = """
|
|
||||||
select ivl, due from cards
|
|
||||||
where queue between 0 and 1 and ivl <= 21"""
|
|
||||||
mature = """
|
|
||||||
select ivl, due
|
|
||||||
from cards where queue = 1 and ivl > 21"""
|
|
||||||
if self.selective:
|
|
||||||
young += self.deck.sched._groupLimit("rev")
|
|
||||||
mature += self.deck.sched._groupLimit("rev")
|
|
||||||
young = self.deck.db.all(young)
|
|
||||||
mature = self.deck.db.all(mature)
|
|
||||||
for (src, dest) in [(young, daysYoung),
|
|
||||||
(mature, daysMature)]:
|
|
||||||
for (ivl, due) in src:
|
|
||||||
day=int(round(ivl))
|
|
||||||
days[day] = days.get(day, 0) + 1
|
|
||||||
indays = int(((due - self.endOfDay) / 86400.0) + 1)
|
|
||||||
next[indays] = next.get(indays, 0) + 1 # type-agnostic stats
|
|
||||||
dest[indays] = dest.get(indays, 0) + 1 # type-specific stats
|
|
||||||
if indays < lowestInDay:
|
|
||||||
lowestInDay = indays
|
|
||||||
self.stats['next'] = next
|
|
||||||
self.stats['days'] = days
|
|
||||||
self.stats['daysByType'] = {'young': daysYoung,
|
|
||||||
'mature': daysMature}
|
|
||||||
self.stats['months'] = months
|
|
||||||
self.stats['lowestInDay'] = lowestInDay
|
|
||||||
dayReps = self._getDayReps()
|
dayReps = self._getDayReps()
|
||||||
# fixme: change 0 to correct offset
|
# fixme: change 0 to correct offset
|
||||||
todaydt = datetime.datetime.utcfromtimestamp(
|
todaydt = datetime.datetime.utcfromtimestamp(
|
||||||
|
@ -206,69 +89,19 @@ from cards where queue = 1 and ivl > 21"""
|
||||||
map(lambda dr: (-(todaydt - datetime.date(
|
map(lambda dr: (-(todaydt - datetime.date(
|
||||||
*(int(x)for x in dr[1].split("-")))).days, dr[4]/60.0), dayReps))
|
*(int(x)for x in dr[1].split("-")))).days, dr[4]/60.0), dayReps))
|
||||||
|
|
||||||
def _dueCards(self, days=7):
|
def _getDayReps(self):
|
||||||
return self.deck.db.all("""
|
return self.deck.db.all("""
|
||||||
select due-:today,
|
select
|
||||||
count(), -- all
|
count() as combinedNewReps,
|
||||||
sum(case when ivl >= 21 then 1 else 0 end) -- mature
|
date(time/1000-:off, "unixepoch") as day,
|
||||||
from cards
|
sum(case when lastIvl > 21 then 1 else 0 end) as matureReps,
|
||||||
where queue = 2 and due < (:today+:days) %s
|
count() - sum(case when rep = 1 then 1 else 0 end) as combinedYoungReps,
|
||||||
group by due order by due""" % self._limit(),
|
sum(taken/1000) as reviewTime from revlog
|
||||||
today=self.deck.sched.today, days=days)
|
group by day order by day
|
||||||
|
""", off=0)
|
||||||
|
|
||||||
def _graph(self, id, data, conf={}, width=600, height=200):
|
# Intervals
|
||||||
return (
|
######################################################################
|
||||||
"""<div id="%(id)s" style="width:%(w)s; height:%(h)s;"></div>
|
|
||||||
<script>
|
|
||||||
$(function () {
|
|
||||||
$.plot($("#%(id)s"), %(data)s, %(conf)s);
|
|
||||||
});
|
|
||||||
</script>""" % dict(
|
|
||||||
id=id, w=width, h=height,
|
|
||||||
data=simplejson.dumps(data),
|
|
||||||
conf=simplejson.dumps(conf)))
|
|
||||||
|
|
||||||
def dueGraph(self):
|
|
||||||
d = self._dueCards()
|
|
||||||
yng = []
|
|
||||||
mtr = []
|
|
||||||
for day in d:
|
|
||||||
yng.append((day[0], day[1]))
|
|
||||||
mtr.append((day[0], day[2]))
|
|
||||||
txt = self._graph(id="due", data=[
|
|
||||||
dict(data=yng, bars=dict(show=True, barWidth=0.8),
|
|
||||||
color=dueYoungC, label=_("Young")),
|
|
||||||
dict(data=mtr, bars=dict(show=True, barWidth=0.8),
|
|
||||||
color=dueMatureC, label=_("Mature"))
|
|
||||||
])
|
|
||||||
self.save(txt)
|
|
||||||
|
|
||||||
def save(self, txt):
|
|
||||||
open(os.path.expanduser("~/test.html"), "w").write("""
|
|
||||||
<html><head>
|
|
||||||
<script src="jquery.min.js"></script>
|
|
||||||
<script src="jquery.flot.min.js"></script>
|
|
||||||
</head><body>%s</body></html>"""%txt)
|
|
||||||
|
|
||||||
def cumDueGraph(self):
|
|
||||||
self._calcStats()
|
|
||||||
d = self._stats['due']
|
|
||||||
tot = 0
|
|
||||||
days = []
|
|
||||||
for day in d:
|
|
||||||
tot += day[1]+day[2]
|
|
||||||
days.append((day[0], tot))
|
|
||||||
txt = self._graph(id="cum", data=[
|
|
||||||
dict(data=days, lines=dict(show=True, fill=True),
|
|
||||||
color=dueCumulC, label=_("Cards")),
|
|
||||||
])
|
|
||||||
self.save(txt)
|
|
||||||
|
|
||||||
def _limit(self):
|
|
||||||
if self.selective:
|
|
||||||
return self.deck.sched._groupLimit("rev")
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _ivls(self):
|
def _ivls(self):
|
||||||
return self.deck.db.all("""
|
return self.deck.db.all("""
|
||||||
|
@ -284,30 +117,88 @@ order by ivl / 7""" % self._limit())
|
||||||
dict(data=ivls, bars=dict(show=True, barWidth=0.8),
|
dict(data=ivls, bars=dict(show=True, barWidth=0.8),
|
||||||
color=intervC)
|
color=intervC)
|
||||||
])
|
])
|
||||||
self.save(txt)
|
return txt
|
||||||
|
|
||||||
def _getDayReps(self):
|
# Eases
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
def _eases(self):
|
||||||
|
# ignores selective, at least for now
|
||||||
return self.deck.db.all("""
|
return self.deck.db.all("""
|
||||||
select
|
select (case
|
||||||
count() as combinedNewReps,
|
when type = 0 then 0
|
||||||
date(time/1000-:off, "unixepoch") as day,
|
when lastIvl <= 21 then 1
|
||||||
sum(case when lastIvl > 21 then 1 else 0 end) as matureReps,
|
else 2 end) as thetype,
|
||||||
count() - sum(case when rep = 1 then 1 else 0 end) as combinedYoungReps,
|
ease, count() from revlog
|
||||||
sum(taken/1000) as reviewTime from revlog
|
group by thetype, ease
|
||||||
group by day order by day
|
order by thetype, ease""")
|
||||||
""", off=0)
|
|
||||||
|
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", data=[
|
||||||
|
dict(data=d['lrn'], bars=dict(show=True, barWidth=0.8, align="center"),
|
||||||
|
color=easesNewC, label=_("Learning")),
|
||||||
|
dict(data=d['yng'], bars=dict(show=True, barWidth=0.8, align="center"),
|
||||||
|
color=easesYoungC, label=_("Young")),
|
||||||
|
dict(data=d['mtr'], bars=dict(show=True, barWidth=0.8, align="center"),
|
||||||
|
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['due'] = self._due()
|
||||||
|
self._stats['ivls'] = self._ivls()
|
||||||
|
self._stats['done'] = self._done()
|
||||||
|
self._stats['eases'] = self._eases()
|
||||||
|
|
||||||
|
def _graph(self, id, data, conf={}, width=600, height=200):
|
||||||
|
return (
|
||||||
|
"""<div id="%(id)s" style="width:%(w)s; height:%(h)s;"></div>
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
$.plot($("#%(id)s"), %(data)s, %(conf)s);
|
||||||
|
});
|
||||||
|
</script>""" % dict(
|
||||||
|
id=id, w=width, h=height,
|
||||||
|
data=simplejson.dumps(data),
|
||||||
|
conf=simplejson.dumps(conf)))
|
||||||
|
|
||||||
|
def _limit(self):
|
||||||
|
if self.selective:
|
||||||
|
return self.deck.sched._groupLimit("rev")
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def save(self, txt):
|
||||||
|
open(os.path.expanduser("~/test.html"), "w").write("""
|
||||||
|
<html><head>
|
||||||
|
<script src="jquery.min.js"></script>
|
||||||
|
<script src="jquery.flot.min.js"></script>
|
||||||
|
</head><body>%s</body></html>"""%txt)
|
||||||
|
|
||||||
def _addMissing(self, dic, min, max):
|
def _addMissing(self, dic, min, max):
|
||||||
for i in range(min, max+1):
|
for i in range(min, max+1):
|
||||||
if not i in dic:
|
if not i in dic:
|
||||||
dic[i] = 0
|
dic[i] = 0
|
||||||
|
|
||||||
def _unzip(self, tuples, fillFix=True, limit=None, reverseLimit=False):
|
|
||||||
tuples.sort(cmp=lambda x,y: cmp(x[0], y[0]))
|
|
||||||
if limit:
|
|
||||||
if reverseLimit:
|
|
||||||
tuples = tuples[-limit:]
|
|
||||||
else:
|
|
||||||
tuples = tuples[:limit+1]
|
|
||||||
new = zip(*tuples)
|
|
||||||
return new
|
|
||||||
|
|
|
@ -26,8 +26,10 @@ def test_graphs():
|
||||||
from anki import Deck
|
from anki import Deck
|
||||||
d = Deck(os.path.expanduser("~/test.anki"))
|
d = Deck(os.path.expanduser("~/test.anki"))
|
||||||
g = d.graphs()
|
g = d.graphs()
|
||||||
g._calcStats()
|
assert g.dueGraph()
|
||||||
g.ivlGraph()
|
assert g.cumDueGraph()
|
||||||
|
assert g.ivlGraph()
|
||||||
|
assert g.easeGraph()
|
||||||
return
|
return
|
||||||
g.nextDue()
|
g.nextDue()
|
||||||
g.workDone()
|
g.workDone()
|
||||||
|
|
Loading…
Reference in a new issue