mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 08:22:24 -04:00
new graph handling
This commit is contained in:
parent
5c337aa658
commit
f20d730f3c
3 changed files with 38 additions and 320 deletions
291
aqt/graphs.py
291
aqt/graphs.py
|
@ -1,291 +0,0 @@
|
||||||
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
||||||
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
|
||||||
|
|
||||||
from PyQt4.QtGui import *
|
|
||||||
from PyQt4.QtCore import *
|
|
||||||
import sys
|
|
||||||
import anki, anki.graphs, anki.utils
|
|
||||||
from aqt import ui
|
|
||||||
from aqt.ui.utils import saveGeom, restoreGeom
|
|
||||||
import aqt
|
|
||||||
|
|
||||||
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
|
|
||||||
from matplotlib import rc
|
|
||||||
rc('font', **{'sans-serif': 'Arial',
|
|
||||||
'serif': 'Arial',
|
|
||||||
'size': 14.0})
|
|
||||||
rc('legend', fontsize=14.0)
|
|
||||||
|
|
||||||
class AnkiFigureCanvas (FigureCanvas):
|
|
||||||
def __init__(self, fig, parent=None):
|
|
||||||
self.fig = fig
|
|
||||||
FigureCanvas.__init__(self, self.fig)
|
|
||||||
self.setParent(parent)
|
|
||||||
|
|
||||||
self.fig.subplots_adjust(left=0.08, right=0.96, bottom=0.15, top=0.95)
|
|
||||||
|
|
||||||
FigureCanvas.setSizePolicy(self,
|
|
||||||
QSizePolicy.Expanding,
|
|
||||||
QSizePolicy.Fixed)
|
|
||||||
FigureCanvas.updateGeometry(self)
|
|
||||||
|
|
||||||
def sizeHint(self):
|
|
||||||
w, h = self.get_width_height()
|
|
||||||
return QSize(w+30, h+30)
|
|
||||||
|
|
||||||
def keyReleaseEvent(self, evt):
|
|
||||||
evt.ignore()
|
|
||||||
|
|
||||||
def keyPressEvent(self, evt):
|
|
||||||
evt.ignore()
|
|
||||||
|
|
||||||
def wheelEvent(self, evt):
|
|
||||||
evt.ignore()
|
|
||||||
|
|
||||||
class AdjustableFigure(QWidget):
|
|
||||||
|
|
||||||
def __init__(self, parent, name, figureFunc, choices=None):
|
|
||||||
QWidget.__init__(self)
|
|
||||||
self.parent = parent
|
|
||||||
self.config = parent.config
|
|
||||||
self.name = name
|
|
||||||
self.vbox = QVBoxLayout()
|
|
||||||
self.vbox.setSpacing(2)
|
|
||||||
self.range = None
|
|
||||||
self.choices = choices
|
|
||||||
self.figureFunc = figureFunc
|
|
||||||
self.setLayout(self.vbox)
|
|
||||||
self.updateTimer = None
|
|
||||||
self.hbox = QHBoxLayout()
|
|
||||||
self.hbox.addSpacing(10)
|
|
||||||
self.hbox.addStretch(1)
|
|
||||||
self.figureCanvas = None
|
|
||||||
if self.choices:
|
|
||||||
self.addCombo()
|
|
||||||
|
|
||||||
def addWidget(self, widget):
|
|
||||||
self.vbox.addWidget(widget)
|
|
||||||
|
|
||||||
def addFigure(self):
|
|
||||||
if self.range is None:
|
|
||||||
self.figureCanvas = AnkiFigureCanvas(self.figureFunc())
|
|
||||||
else:
|
|
||||||
if self.range:
|
|
||||||
self.figureCanvas = AnkiFigureCanvas(self.figureFunc(self.range))
|
|
||||||
else:
|
|
||||||
self.figureCanvas = AnkiFigureCanvas(self.figureFunc())
|
|
||||||
self.addWidget(self.figureCanvas)
|
|
||||||
self.vbox.addLayout(self.hbox)
|
|
||||||
|
|
||||||
def updateFigure(self):
|
|
||||||
if self.parent.inDbHandler:
|
|
||||||
return
|
|
||||||
self.updateTimer = None
|
|
||||||
self.setUpdatesEnabled(False)
|
|
||||||
idx = self.vbox.indexOf(self.figureCanvas)
|
|
||||||
self.vbox.removeWidget(self.figureCanvas)
|
|
||||||
if not self.figureCanvas:
|
|
||||||
self.addFigure()
|
|
||||||
else:
|
|
||||||
self.figureCanvas.deleteLater()
|
|
||||||
if self.range:
|
|
||||||
self.figureCanvas = AnkiFigureCanvas(self.figureFunc(self.range))
|
|
||||||
else:
|
|
||||||
self.figureCanvas = AnkiFigureCanvas(self.figureFunc())
|
|
||||||
self.vbox.insertWidget(idx, self.figureCanvas)
|
|
||||||
self.setUpdatesEnabled(True)
|
|
||||||
|
|
||||||
def addCombo(self):
|
|
||||||
self.periodCombo = QComboBox()
|
|
||||||
self.periodCombo.addItems(QStringList(
|
|
||||||
[anki.utils.fmtTimeSpan(x*86400, point = -1) for x in self.choices]))
|
|
||||||
self.hbox.addWidget(self.periodCombo)
|
|
||||||
idx = self.config.get('graphs.period.' + self.name, 1)
|
|
||||||
self.periodCombo.setCurrentIndex(idx)
|
|
||||||
self.connect(self.periodCombo, SIGNAL("currentIndexChanged(int)"),
|
|
||||||
self.onPeriodChange)
|
|
||||||
self.onPeriodChange(idx, initialSkip=True)
|
|
||||||
|
|
||||||
def onPeriodChange(self, index, initialSkip=False):
|
|
||||||
self.config['graphs.period.' + self.name] = index
|
|
||||||
self.range = self.choices[index]
|
|
||||||
if not initialSkip:
|
|
||||||
self.scheduleUpdate()
|
|
||||||
|
|
||||||
def scheduleUpdate(self):
|
|
||||||
if not self.updateTimer:
|
|
||||||
self.updateTimer = QTimer(self)
|
|
||||||
self.updateTimer.setSingleShot(True)
|
|
||||||
self.updateTimer.start(200)
|
|
||||||
self.connect(self.updateTimer, SIGNAL("timeout()"),
|
|
||||||
self.updateFigure)
|
|
||||||
else:
|
|
||||||
self.updateTimer.setInterval(200)
|
|
||||||
|
|
||||||
def addExplanation(self, text):
|
|
||||||
self.explanation = QLabel(text)
|
|
||||||
self.hbox.insertWidget(1, self.explanation)
|
|
||||||
|
|
||||||
def showHide(self):
|
|
||||||
shown = self.config.get('graphs.shown.' + self.name, True)
|
|
||||||
self.setVisible(shown)
|
|
||||||
if shown and not self.figureCanvas:
|
|
||||||
self.addFigure()
|
|
||||||
|
|
||||||
class IntervalGraph(QDialog):
|
|
||||||
|
|
||||||
def __init__(self, parent, *args):
|
|
||||||
QDialog.__init__(self, parent, Qt.Window)
|
|
||||||
ui.dialogs.open("Graphs", self)
|
|
||||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
|
||||||
|
|
||||||
def reject(self):
|
|
||||||
saveGeom(self, "graphs")
|
|
||||||
ui.dialogs.close("Graphs")
|
|
||||||
QDialog.reject(self)
|
|
||||||
|
|
||||||
class GraphWindow(object):
|
|
||||||
|
|
||||||
nameMap = {
|
|
||||||
'due': _("Due"),
|
|
||||||
'cum': _("Cumulative"),
|
|
||||||
'interval': _("Interval"),
|
|
||||||
'added': _("Added"),
|
|
||||||
'answered': _("First Answered"),
|
|
||||||
'eases': _("Eases"),
|
|
||||||
'reps': _("Repetitions"),
|
|
||||||
'times': _("Review Time"),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, parent, deck):
|
|
||||||
self.parent = parent
|
|
||||||
self.deck = deck
|
|
||||||
self.widgets = []
|
|
||||||
self.dg = anki.graphs.DeckGraphs(deck)
|
|
||||||
self.diag = IntervalGraph(parent)
|
|
||||||
self.diag.setWindowTitle(_("Deck Graphs"))
|
|
||||||
if parent.config.get('graphsGeom'):
|
|
||||||
restoreGeom(self.diag, "graphs")
|
|
||||||
else:
|
|
||||||
if sys.platform.startswith("darwin"):
|
|
||||||
self.diag.setMinimumSize(740, 680)
|
|
||||||
else:
|
|
||||||
self.diag.setMinimumSize(690, 715)
|
|
||||||
scroll = QScrollArea(self.diag)
|
|
||||||
topBox = QVBoxLayout(self.diag)
|
|
||||||
topBox.addWidget(scroll)
|
|
||||||
self.frame = QWidget(scroll)
|
|
||||||
self.vbox = QVBoxLayout(self.frame)
|
|
||||||
self.vbox.setMargin(0)
|
|
||||||
self.vbox.setSpacing(0)
|
|
||||||
self.frame.setLayout(self.vbox)
|
|
||||||
self.range = [7, 30, 90, 180, 365, 730, 1095, 1460, 1825]
|
|
||||||
scroll.setWidget(self.frame)
|
|
||||||
self.hbox = QHBoxLayout()
|
|
||||||
topBox.addLayout(self.hbox)
|
|
||||||
self.setupGraphs()
|
|
||||||
self.setupButtons()
|
|
||||||
self.showHideAll()
|
|
||||||
self.diag.show()
|
|
||||||
|
|
||||||
def setupGraphs(self):
|
|
||||||
nextDue = AdjustableFigure(self.parent, 'due', self.dg.nextDue, self.range)
|
|
||||||
nextDue.addWidget(QLabel(_("<h1>Due</h1>")))
|
|
||||||
self.vbox.addWidget(nextDue)
|
|
||||||
self.widgets.append(nextDue)
|
|
||||||
|
|
||||||
workload = AdjustableFigure(self.parent, 'reps', self.dg.workDone, self.range)
|
|
||||||
workload.addWidget(QLabel(_("<h1>Reps</h1>")))
|
|
||||||
self.vbox.addWidget(workload)
|
|
||||||
self.widgets.append(workload)
|
|
||||||
|
|
||||||
times = AdjustableFigure(self.parent, 'times', self.dg.timeSpent, self.range)
|
|
||||||
times.addWidget(QLabel(_("<h1>Review Time</h1>")))
|
|
||||||
self.vbox.addWidget(times)
|
|
||||||
self.widgets.append(times)
|
|
||||||
|
|
||||||
added = AdjustableFigure(self.parent, 'added', self.dg.addedRecently, self.range)
|
|
||||||
added.addWidget(QLabel(_("<h1>Added</h1>")))
|
|
||||||
self.vbox.addWidget(added)
|
|
||||||
self.widgets.append(added)
|
|
||||||
|
|
||||||
answered = AdjustableFigure(self.parent, 'answered', lambda *args: apply(
|
|
||||||
self.dg.addedRecently, args + ('firstAnswered',)), self.range)
|
|
||||||
answered.addWidget(QLabel(_("<h1>First Answered</h1>")))
|
|
||||||
self.vbox.addWidget(answered)
|
|
||||||
self.widgets.append(answered)
|
|
||||||
|
|
||||||
cumDue = AdjustableFigure(self.parent, 'cum', self.dg.cumulativeDue, self.range)
|
|
||||||
cumDue.addWidget(QLabel(_("<h1>Cumulative Due</h1>")))
|
|
||||||
self.vbox.addWidget(cumDue)
|
|
||||||
self.widgets.append(cumDue)
|
|
||||||
|
|
||||||
interval = AdjustableFigure(self.parent, 'interval', self.dg.intervalPeriod, self.range)
|
|
||||||
interval.addWidget(QLabel(_("<h1>Intervals</h1>")))
|
|
||||||
self.vbox.addWidget(interval)
|
|
||||||
self.widgets.append(interval)
|
|
||||||
|
|
||||||
eases = AdjustableFigure(self.parent, 'eases', self.dg.easeBars)
|
|
||||||
eases.addWidget(QLabel(_("<h1>Eases</h1>")))
|
|
||||||
self.vbox.addWidget(eases)
|
|
||||||
self.widgets.append(eases)
|
|
||||||
|
|
||||||
def setupButtons(self):
|
|
||||||
self.showhide = QPushButton(_("Show/Hide"))
|
|
||||||
self.hbox.addWidget(self.showhide)
|
|
||||||
self.showhide.connect(self.showhide, SIGNAL("clicked()"),
|
|
||||||
self.onShowHide)
|
|
||||||
refresh = QPushButton(_("Refresh"))
|
|
||||||
self.hbox.addWidget(refresh)
|
|
||||||
self.showhide.connect(refresh, SIGNAL("clicked()"),
|
|
||||||
self.onRefresh)
|
|
||||||
buttonBox = QDialogButtonBox(self.diag)
|
|
||||||
buttonBox.setOrientation(Qt.Horizontal)
|
|
||||||
close = buttonBox.addButton(QDialogButtonBox.Close)
|
|
||||||
close.setDefault(True)
|
|
||||||
self.diag.connect(buttonBox, SIGNAL("rejected()"), self.diag.close)
|
|
||||||
help = buttonBox.addButton(QDialogButtonBox.Help)
|
|
||||||
self.diag.connect(buttonBox, SIGNAL("helpRequested()"),
|
|
||||||
self.onHelp)
|
|
||||||
self.hbox.addWidget(buttonBox)
|
|
||||||
|
|
||||||
def showHideAll(self):
|
|
||||||
self.mw.startProgress(len(self.widgets))
|
|
||||||
for w in self.widgets:
|
|
||||||
self.deck.updateProgress(_("Processing..."))
|
|
||||||
w.showHide()
|
|
||||||
self.frame.adjustSize()
|
|
||||||
self.deck.finishProgress()
|
|
||||||
|
|
||||||
def onShowHideToggle(self, b, w):
|
|
||||||
key = 'graphs.shown.' + w.name
|
|
||||||
self.parent.config[key] = not self.parent.config.get(key, True)
|
|
||||||
self.showHideAll()
|
|
||||||
|
|
||||||
def onShowHide(self):
|
|
||||||
m = QMenu(self.parent)
|
|
||||||
for graph in self.widgets:
|
|
||||||
name = graph.name
|
|
||||||
shown = self.parent.config.get('graphs.shown.' + name, True)
|
|
||||||
action = QAction(self.parent)
|
|
||||||
action.setCheckable(True)
|
|
||||||
action.setChecked(shown)
|
|
||||||
action.setText(self.nameMap[name])
|
|
||||||
action.connect(action, SIGNAL("toggled(bool)"),
|
|
||||||
lambda b, g=graph: self.onShowHideToggle(b, g))
|
|
||||||
m.addAction(action)
|
|
||||||
m.exec_(self.showhide.mapToGlobal(QPoint(0,0)))
|
|
||||||
|
|
||||||
def onHelp(self):
|
|
||||||
QDesktopServices.openUrl(QUrl(aqt.appWiki + "Graphs"))
|
|
||||||
|
|
||||||
def onRefresh(self):
|
|
||||||
self.mw.startProgress(len(self.widgets))
|
|
||||||
self.dg.stats = None
|
|
||||||
for w in self.widgets:
|
|
||||||
self.deck.updateProgress(_("Processing..."))
|
|
||||||
w.updateFigure()
|
|
||||||
self.deck.finishProgress()
|
|
||||||
|
|
||||||
def intervalGraph(*args):
|
|
||||||
return GraphWindow(*args)
|
|
29
aqt/main.py
29
aqt/main.py
|
@ -1166,7 +1166,7 @@ learnt today")
|
||||||
def rmDockable(self, dock):
|
def rmDockable(self, dock):
|
||||||
self.removeDockWidget(dock)
|
self.removeDockWidget(dock)
|
||||||
|
|
||||||
# Card & deck stats
|
# Stats and graphs
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def setupCardStats(self):
|
def setupCardStats(self):
|
||||||
|
@ -1178,29 +1178,8 @@ learnt today")
|
||||||
def onDeckStats(self):
|
def onDeckStats(self):
|
||||||
aqt.stats.deckStats(self)
|
aqt.stats.deckStats(self)
|
||||||
|
|
||||||
# Graphs
|
def onGraphs(self):
|
||||||
##########################################################################
|
aqt.stats.graphs(self)
|
||||||
|
|
||||||
def onShowGraph(self):
|
|
||||||
self.setStatus(_("Loading graphs (may take time)..."))
|
|
||||||
self.app.processEvents()
|
|
||||||
import anki.graphs
|
|
||||||
if anki.graphs.graphsAvailable():
|
|
||||||
try:
|
|
||||||
aqt.dialogs.open("Graphs", self, self.deck)
|
|
||||||
except (ImportError, ValueError):
|
|
||||||
traceback.print_exc()
|
|
||||||
if sys.platform.startswith("win32"):
|
|
||||||
showInfo(
|
|
||||||
_("To display graphs, Anki needs a .dll file which\n"
|
|
||||||
"you don't have. Please install:\n") +
|
|
||||||
"http://www.dll-files.com/dllindex/dll-files.shtml?msvcp71")
|
|
||||||
else:
|
|
||||||
showInfo(_(
|
|
||||||
"Your version of Matplotlib is broken.\n"
|
|
||||||
"Please see http://ichi2.net/anki/wiki/MatplotlibBroken"))
|
|
||||||
else:
|
|
||||||
showInfo(_("Please install python-matplotlib to access graphs."))
|
|
||||||
|
|
||||||
# Marking, suspending and undoing
|
# Marking, suspending and undoing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -1791,7 +1770,7 @@ This deck already exists on your computer. Overwrite the local copy?"""),
|
||||||
self.connect(m.actionPreferences, s, self.onPrefs)
|
self.connect(m.actionPreferences, s, self.onPrefs)
|
||||||
self.connect(m.actionDstats, s, self.onDeckStats)
|
self.connect(m.actionDstats, s, self.onDeckStats)
|
||||||
self.connect(m.actionCstats, s, self.onCardStats)
|
self.connect(m.actionCstats, s, self.onCardStats)
|
||||||
self.connect(m.actionGraphs, s, self.onShowGraph)
|
self.connect(m.actionGraphs, s, self.onGraphs)
|
||||||
self.connect(m.actionEditLayout, s, self.onCardLayout)
|
self.connect(m.actionEditLayout, s, self.onCardLayout)
|
||||||
self.connect(m.actionAbout, s, self.onAbout)
|
self.connect(m.actionAbout, s, self.onAbout)
|
||||||
self.connect(m.actionStarthere, s, self.onStartHere)
|
self.connect(m.actionStarthere, s, self.onStartHere)
|
||||||
|
|
38
aqt/stats.py
38
aqt/stats.py
|
@ -58,16 +58,17 @@ class CardStats(object):
|
||||||
<style>table { font-size: 12px; } h1 { font-size: 14px; }</style>
|
<style>table { font-size: 12px; } h1 { font-size: 14px; }</style>
|
||||||
</head><body><center>%s</center></body></html>"""%txt)
|
</head><body><center>%s</center></body></html>"""%txt)
|
||||||
|
|
||||||
# Deck stats
|
# Modal dialog that supports dumping to browser (for printing, etc)
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
class PrintableReport(QDialog):
|
class PrintableReport(QDialog):
|
||||||
|
|
||||||
def __init__(self, mw, type, title, func, css):
|
def __init__(self, mw, type, title, func, css, extra):
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
QDialog.__init__(self, mw)
|
QDialog.__init__(self, mw)
|
||||||
restoreGeom(self, type)
|
restoreGeom(self, type)
|
||||||
self.type = type
|
self.type = type
|
||||||
|
self.extra = extra
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
self.mw.progress.start()
|
self.mw.progress.start()
|
||||||
|
@ -99,10 +100,13 @@ class PrintableReport(QDialog):
|
||||||
tmpdir = tempfile.mkdtemp(prefix="anki")
|
tmpdir = tempfile.mkdtemp(prefix="anki")
|
||||||
path = os.path.join(tmpdir, "report.html")
|
path = os.path.join(tmpdir, "report.html")
|
||||||
open(path, "w").write("""
|
open(path, "w").write("""
|
||||||
<html><head><style>%s</style></head><body>%s</body></html>""" % (
|
<html><head><style>%s</style>%s</head><body>%s</body></html>""" % (
|
||||||
self.css, self.report))
|
self.css, self.extra, self.report))
|
||||||
QDesktopServices.openUrl(QUrl("file://" + path))
|
QDesktopServices.openUrl(QUrl("file://" + path))
|
||||||
|
|
||||||
|
# Deck stats
|
||||||
|
######################################################################
|
||||||
|
|
||||||
def deckStats(mw):
|
def deckStats(mw):
|
||||||
css=mw.sharedCSS+"""
|
css=mw.sharedCSS+"""
|
||||||
body { margin: 2em; font-family: arial; }
|
body { margin: 2em; font-family: arial; }
|
||||||
|
@ -117,3 +121,29 @@ h1 { font-size: 18px; border-bottom: 1px solid #000; margin-top: 1em;
|
||||||
_("Deck Statistics"),
|
_("Deck Statistics"),
|
||||||
mw.deck.deckStats,
|
mw.deck.deckStats,
|
||||||
css)
|
css)
|
||||||
|
|
||||||
|
# Graphs
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
def graphs(mw):
|
||||||
|
css=mw.sharedCSS+"""
|
||||||
|
body { margin: 2em; font-family: arial; background: #eee; }
|
||||||
|
h1 { font-size: 18px; border-bottom: 1px solid #000; margin-top: 1em;
|
||||||
|
clear: both; margin-bottom: 0.5em; }
|
||||||
|
.info {float:right; padding: 10px; max-width: 300px; border-radius: 5px;
|
||||||
|
background: #ddd; font-size: 14px; }
|
||||||
|
"""
|
||||||
|
buf = "<script>"
|
||||||
|
for n in ("jquery", "jquery.flot"):
|
||||||
|
f = QFile(":/%s.min.js" % n)
|
||||||
|
f.open(QIODevice.ReadOnly)
|
||||||
|
buf += f.readAll()
|
||||||
|
f.close()
|
||||||
|
buf += "</script>"
|
||||||
|
return PrintableReport(
|
||||||
|
mw,
|
||||||
|
"graphs",
|
||||||
|
_("Graphs"),
|
||||||
|
lambda: mw.deck.graphs().report(),
|
||||||
|
css,
|
||||||
|
buf)
|
||||||
|
|
Loading…
Reference in a new issue