mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
due and cumulative due graphs ported
This commit is contained in:
parent
2ca9568196
commit
89fa08c548
2 changed files with 119 additions and 217 deletions
238
anki/graphs.py
238
anki/graphs.py
|
@ -2,89 +2,31 @@
|
|||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
||||
|
||||
import os, sys, time, datetime
|
||||
import os, sys, time, datetime, simplejson
|
||||
from anki.lang import _
|
||||
|
||||
#colours for graphs
|
||||
dueYoungC = "#ffb380"
|
||||
dueMatureC = "#ff5555"
|
||||
dueCumulC = "#ff8080"
|
||||
|
||||
reviewNewC = "#80ccff"
|
||||
reviewYoungC = "#3377ff"
|
||||
reviewMatureC = "#0000ff"
|
||||
reviewTimeC = "#0fcaff"
|
||||
|
||||
easesNewC = "#80b3ff"
|
||||
easesYoungC = "#5555ff"
|
||||
easesMatureC = "#0f5aff"
|
||||
|
||||
addedC = "#b3ff80"
|
||||
firstC = "#b380ff"
|
||||
intervC = "#80e5ff"
|
||||
|
||||
# support frozen distribs
|
||||
if sys.platform.startswith("darwin"):
|
||||
try:
|
||||
del os.environ['MATPLOTLIBDATA']
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
from matplotlib.figure import Figure
|
||||
except UnicodeEncodeError:
|
||||
# haven't tracked down the cause of this yet, but reloading fixes it
|
||||
try:
|
||||
from matplotlib.figure import Figure
|
||||
except ImportError:
|
||||
pass
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def graphsAvailable():
|
||||
return 'matplotlib' in sys.modules
|
||||
|
||||
class Graphs(object):
|
||||
|
||||
def __init__(self, deck, width=8, height=3, dpi=75, selective=True):
|
||||
def __init__(self, deck, selective=True):
|
||||
self.deck = deck
|
||||
self.stats = None
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.dpi = dpi
|
||||
self._stats = None
|
||||
self.selective = selective
|
||||
|
||||
def nextDue(self, days=30):
|
||||
self._calcStats()
|
||||
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
|
||||
graph = fig.add_subplot(111)
|
||||
dayslists = [self.stats['next'], self.stats['daysByType']['mature']]
|
||||
|
||||
for dayslist in dayslists:
|
||||
self._addMissing(dayslist, self.stats['lowestInDay'], days)
|
||||
|
||||
argl = []
|
||||
|
||||
for dayslist in dayslists:
|
||||
dl = [x for x in dayslist.items() if x[0] <= days]
|
||||
argl.extend(list(self._unzip(dl)))
|
||||
|
||||
self._varGraph(graph, days, [dueYoungC, dueMatureC], *argl)
|
||||
|
||||
cheat = fig.add_subplot(111)
|
||||
b1 = cheat.bar(0, 0, color = dueYoungC)
|
||||
b2 = cheat.bar(1, 0, color = dueMatureC)
|
||||
|
||||
cheat.legend([b1, b2], [
|
||||
"Young",
|
||||
"Mature"], loc='upper right')
|
||||
|
||||
graph.set_xlim(xmin=self.stats['lowestInDay'], xmax=days+1)
|
||||
graph.set_xlabel("Day (0 = today)")
|
||||
graph.set_ylabel("Cards Due")
|
||||
|
||||
return fig
|
||||
|
||||
def workDone(self, days=30):
|
||||
self._calcStats()
|
||||
|
||||
|
@ -130,29 +72,6 @@ class Graphs(object):
|
|||
graph.set_ylabel("Minutes")
|
||||
return fig
|
||||
|
||||
def cumulativeDue(self, days=30):
|
||||
self._calcStats()
|
||||
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
|
||||
graph = fig.add_subplot(111)
|
||||
self._addMissing(self.stats['next'], 0, days-1)
|
||||
dl = [x for x in self.stats['next'].items() if x[0] <= days]
|
||||
(x, y) = self._unzip(dl)
|
||||
count=0
|
||||
y = list(y)
|
||||
for i in range(len(x)):
|
||||
count = count + y[i]
|
||||
if i == 0:
|
||||
continue
|
||||
y[i] = count
|
||||
if x[i] > days:
|
||||
break
|
||||
self._filledGraph(graph, days, dueCumulC, 1, x, y)
|
||||
graph.set_xlim(xmin=self.stats['lowestInDay'], xmax=days-1)
|
||||
graph.set_ylim(ymax=graph.get_ylim()[1]+10)
|
||||
graph.set_xlabel("Day (0 = today)")
|
||||
graph.set_ylabel("Cards Due")
|
||||
return fig
|
||||
|
||||
def ivlPeriod(self, days=30):
|
||||
self._calcStats()
|
||||
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
|
||||
|
@ -244,7 +163,12 @@ as type, ease, count() from revlog group by type, ease""")
|
|||
return fig
|
||||
|
||||
def _calcStats(self):
|
||||
if not self.stats:
|
||||
if self._stats:
|
||||
return
|
||||
self._stats = {}
|
||||
self._stats['due'] = self._dueCards()
|
||||
return
|
||||
|
||||
days = {}
|
||||
daysYoung = {}
|
||||
daysMature = {}
|
||||
|
@ -274,16 +198,13 @@ from cards where queue = 1 and ivl > 21"""
|
|||
dest[indays] = dest.get(indays, 0) + 1 # type-specific stats
|
||||
if indays < lowestInDay:
|
||||
lowestInDay = indays
|
||||
self.stats = {}
|
||||
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()
|
||||
|
||||
# fixme: change 0 to correct offset
|
||||
todaydt = datetime.datetime.utcfromtimestamp(
|
||||
time.time() - 0).date()
|
||||
|
@ -293,11 +214,72 @@ from cards where queue = 1 and ivl > 21"""
|
|||
self.stats[dest] = dict(
|
||||
map(lambda dr: (-(todaydt - datetime.date(
|
||||
*(int(x)for x in dr[1].split("-")))).days, dr[source]), dayReps))
|
||||
|
||||
self.stats['dayTimes'] = dict(
|
||||
map(lambda dr: (-(todaydt - datetime.date(
|
||||
*(int(x)for x in dr[1].split("-")))).days, dr[4]/60.0), dayReps))
|
||||
|
||||
def _dueCards(self, days=7):
|
||||
if self.selective:
|
||||
extra = self.deck.sched._groupLimit("rev")
|
||||
else:
|
||||
extra = ""
|
||||
return self.deck.db.all("""
|
||||
select due-:today,
|
||||
count(), -- all
|
||||
sum(case when ivl >= 21 then 1 else 0 end) -- mature
|
||||
from cards
|
||||
where queue = 2 and due < (:today+:days) %s
|
||||
group by due order by due""" % extra,
|
||||
today=self.deck.sched.today, days=days)
|
||||
|
||||
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 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="due", data=[
|
||||
dict(data=days, lines=dict(show=True, fill=True),
|
||||
color=dueCumulC, label=_("Cards")),
|
||||
])
|
||||
self.save(txt)
|
||||
|
||||
def _getDayReps(self):
|
||||
return self.deck.db.all("""
|
||||
select
|
||||
|
@ -323,81 +305,3 @@ group by day order by day
|
|||
tuples = tuples[:limit+1]
|
||||
new = zip(*tuples)
|
||||
return new
|
||||
|
||||
def _varGraph(self, graph, days, colours=["b"], *args):
|
||||
if len(args[0]) < 120:
|
||||
return self._barGraph(graph, days, colours, *args)
|
||||
else:
|
||||
return self._filledGraph(graph, days, colours, *args)
|
||||
|
||||
def _filledGraph(self, graph, days, colours=["b"], *args):
|
||||
self._filledGraph1(graph, days, colours, 0, *args)
|
||||
|
||||
def _filledGraph1(self, graph, days, colours, lw, *args):
|
||||
if isinstance(colours, str):
|
||||
colours = [colours]
|
||||
print args, colours
|
||||
for triplet in [(args[n], args[n + 1], colours[n / 2]) for n in range(0, len(args), 2)]:
|
||||
x = list(triplet[0])
|
||||
y = list(triplet[1])
|
||||
c = triplet[2]
|
||||
lowest = 99999
|
||||
highest = -lowest
|
||||
for i in range(len(x)):
|
||||
if x[i] < lowest:
|
||||
lowest = x[i]
|
||||
if x[i] > highest:
|
||||
highest = x[i]
|
||||
# ensure the filled area reaches the bottom
|
||||
x.insert(0, lowest - 1)
|
||||
y.insert(0, 0)
|
||||
x.append(highest + 1)
|
||||
y.append(0)
|
||||
# plot
|
||||
graph.fill(x, y, c, lw=lw)
|
||||
graph.grid(True)
|
||||
graph.set_ylim(ymin=0, ymax=max(2, graph.get_ylim()[1]))
|
||||
|
||||
def _barGraph(self, graph, days, colours, *args):
|
||||
if isinstance(colours, str):
|
||||
colours = [colours]
|
||||
lim = None
|
||||
for triplet in [(args[n], args[n + 1], colours[n / 2]) for n in range(0, len(args), 2)]:
|
||||
x = list(triplet[0])
|
||||
y = list(triplet[1])
|
||||
c = triplet[2]
|
||||
lw = 0
|
||||
if lim is None:
|
||||
lim = (x[0], x[-1])
|
||||
length = (lim[1] - lim[0])
|
||||
if len(args) > 4:
|
||||
if length <= 30:
|
||||
lw = 1
|
||||
else:
|
||||
if length <= 90:
|
||||
lw = 1
|
||||
lowest = 99999
|
||||
highest = -lowest
|
||||
for i in range(len(x)):
|
||||
if x[i] < lowest:
|
||||
lowest = x[i]
|
||||
if x[i] > highest:
|
||||
highest = x[i]
|
||||
graph.bar(x, y, color=c, width=1, linewidth=lw)
|
||||
graph.grid(True)
|
||||
graph.set_ylim(ymin=0, ymax=max(2, graph.get_ylim()[1]))
|
||||
import numpy as np
|
||||
if length > 10:
|
||||
step = length / 10.0
|
||||
# python's range() won't accept float step args, so we do it manually
|
||||
if lim[0] < 0:
|
||||
ticks = [int(lim[1] - step * x) for x in range(10)]
|
||||
else:
|
||||
ticks = [int(lim[0] + step * x) for x in range(10)]
|
||||
else:
|
||||
ticks = list(xrange(lim[0], lim[1]+1))
|
||||
graph.set_xticks(np.array(ticks) + 0.5)
|
||||
graph.set_xticklabels([str(int(x)) for x in ticks])
|
||||
for tick in graph.xaxis.get_major_ticks():
|
||||
tick.tick1On = False
|
||||
tick.tick2On = False
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# coding: utf-8
|
||||
|
||||
import time, copy
|
||||
import time, copy, os
|
||||
from tests.shared import assertException, getEmptyDeck
|
||||
from anki.stdmodels import BasicModel
|
||||
from anki.utils import stripHTML, intTime
|
||||
|
@ -23,14 +23,12 @@ def test_stats():
|
|||
assert d.deckStats()
|
||||
|
||||
def test_graphs():
|
||||
d = getEmptyDeck()
|
||||
f = d.newFact()
|
||||
f['Front'] = "foo"
|
||||
d.addFact(f)
|
||||
d.reset()
|
||||
d.sched.answerCard(d.sched.getCard(), 1)
|
||||
c = f.cards()[0]
|
||||
from anki import Deck
|
||||
d = Deck(os.path.expanduser("~/test.anki"))
|
||||
g = d.graphs()
|
||||
g._calcStats()
|
||||
g.cumDueGraph()
|
||||
return
|
||||
g.nextDue()
|
||||
g.workDone()
|
||||
g.timeSpent()
|
||||
|
|
Loading…
Reference in a new issue