new graph handling

This commit is contained in:
Damien Elmes 2011-03-25 15:35:54 +09:00
parent 5c337aa658
commit f20d730f3c
3 changed files with 38 additions and 320 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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)