# -*- coding: utf-8 -*- # Copyright: Damien Elmes # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html """\ Graphs of deck statistics ============================== """ __docformat__ = 'restructuredtext' import os, sys, time import oldanki.stats from oldanki.lang import _ import datetime #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 DeckGraphs(object): def __init__(self, deck, width=8, height=3, dpi=75, selective=True): self.deck = deck self.stats = None self.width = width self.height = height self.dpi = dpi self.selective = selective def calcStats (self): if not self.stats: days = {} daysYoung = {} daysMature = {} months = {} next = {} lowestInDay = 0 self.endOfDay = self.deck.failedCutoff t = time.time() young = """ select interval, combinedDue from cards c where relativeDelay between 0 and 1 and type >= 0 and interval <= 21""" mature = """ select interval, combinedDue from cards c where relativeDelay = 1 and type >= 0 and interval > 21""" if self.selective: young = self.deck._cardLimit("revActive", "revInactive", young) mature = self.deck._cardLimit("revActive", "revInactive", mature) young = self.deck.s.all(young) mature = self.deck.s.all(mature) for (src, dest) in [(young, daysYoung), (mature, daysMature)]: for (interval, due) in src: day=int(round(interval)) 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 = {} 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.deck.s.all(""" select day, matureEase0+matureEase1+matureEase2+matureEase3+matureEase4 as matureReps, reps-(newEase0+newEase1+newEase2+newEase3+newEase4) as combinedYoungReps, reps as combinedNewReps from stats where type = 1""") dayTimes = self.deck.s.all(""" select day, reviewTime as reviewTime from stats where type = 1""") todaydt = self.deck._dailyStats.day for dest, source in [("dayRepsNew", "combinedNewReps"), ("dayRepsYoung", "combinedYoungReps"), ("dayRepsMature", "matureReps")]: self.stats[dest] = dict( map(lambda dr: (-(todaydt -datetime.date( *(int(x)for x in dr["day"].split("-")))).days, dr[source]), dayReps)) self.stats['dayTimes'] = dict( map(lambda dr: (-(todaydt -datetime.date( *(int(x)for x in dr["day"].split("-")))).days, dr["reviewTime"]/60.0), dayTimes)) 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() for type in ["dayRepsNew", "dayRepsYoung", "dayRepsMature"]: 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() fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) times = self.stats['dayTimes'] self.addMissing(times, -days+1, 0) times = self.unzip([(day,y) for (day,y) in times.items() if day + days >= 0]) graph = fig.add_subplot(111) self.varGraph(graph, days, reviewTimeC, *times) graph.set_xlim(xmin=-days+1, xmax=1) graph.set_ylim(ymax=max(a for a in times[1]) + 0.1) graph.set_xlabel("Day (0 = today)") 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 intervalPeriod(self, days=30): self.calcStats() fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) ints = self.stats['days'] self.addMissing(ints, 0, days) intervals = self.unzip(ints.items(), limit=days) graph = fig.add_subplot(111) self.varGraph(graph, days, intervC, *intervals) graph.set_xlim(xmin=0, xmax=days+1) graph.set_xlabel("Card Interval") graph.set_ylabel("Number of Cards") return fig def addedRecently(self, numdays=30, attr='created'): self.calcStats() days = {} fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) limit = self.endOfDay - (numdays) * 86400 res = self.deck.s.column0("select %s from cards where %s >= %f" % (attr, attr, limit)) 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) intervals = self.unzip(days.items()) if attr == 'created': colour = addedC else: colour = firstC self.varGraph(graph, numdays, colour, *intervals) 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 addMissing(self, dic, min, max): for i in range(min, max+1): if not i in dic: 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 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._filledGraph(graph, days, colours, 0, *args) def _filledGraph(self, graph, days, colours, lw, *args): if isinstance(colours, str): colours = [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 def easeBars(self): 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 n = 0 colours = [easesNewC, easesYoungC, easesMatureC] bars = [] gs = oldanki.stats.globalStats(self.deck) for type in types: total = (getattr(gs, type + "Ease0") + getattr(gs, type + "Ease1") + getattr(gs, type + "Ease2") + getattr(gs, type + "Ease3") + getattr(gs, type + "Ease4")) setattr(gs, type + "Ease1", getattr(gs, type + "Ease0") + getattr(gs, type + "Ease1")) setattr(gs, type + "Ease0", -1) for e in range(1, enum): try: arr[e+offset] = (getattr(gs, type + "Ease%d" % e) / float(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 n += 1 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