From f20d730f3ca7e174a6258d08967d707df304fdab Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 25 Mar 2011 15:35:54 +0900 Subject: [PATCH] new graph handling --- aqt/graphs.py | 291 -------------------------------------------------- aqt/main.py | 29 +---- aqt/stats.py | 38 ++++++- 3 files changed, 38 insertions(+), 320 deletions(-) delete mode 100644 aqt/graphs.py diff --git a/aqt/graphs.py b/aqt/graphs.py deleted file mode 100644 index 59e4ff2aa..000000000 --- a/aqt/graphs.py +++ /dev/null @@ -1,291 +0,0 @@ -# Copyright: Damien Elmes -# 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(_("

Due

"))) - self.vbox.addWidget(nextDue) - self.widgets.append(nextDue) - - workload = AdjustableFigure(self.parent, 'reps', self.dg.workDone, self.range) - workload.addWidget(QLabel(_("

Reps

"))) - self.vbox.addWidget(workload) - self.widgets.append(workload) - - times = AdjustableFigure(self.parent, 'times', self.dg.timeSpent, self.range) - times.addWidget(QLabel(_("

Review Time

"))) - self.vbox.addWidget(times) - self.widgets.append(times) - - added = AdjustableFigure(self.parent, 'added', self.dg.addedRecently, self.range) - added.addWidget(QLabel(_("

Added

"))) - 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(_("

First Answered

"))) - self.vbox.addWidget(answered) - self.widgets.append(answered) - - cumDue = AdjustableFigure(self.parent, 'cum', self.dg.cumulativeDue, self.range) - cumDue.addWidget(QLabel(_("

Cumulative Due

"))) - self.vbox.addWidget(cumDue) - self.widgets.append(cumDue) - - interval = AdjustableFigure(self.parent, 'interval', self.dg.intervalPeriod, self.range) - interval.addWidget(QLabel(_("

Intervals

"))) - self.vbox.addWidget(interval) - self.widgets.append(interval) - - eases = AdjustableFigure(self.parent, 'eases', self.dg.easeBars) - eases.addWidget(QLabel(_("

Eases

"))) - 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) diff --git a/aqt/main.py b/aqt/main.py index b1fc6800d..59891e84e 100755 --- a/aqt/main.py +++ b/aqt/main.py @@ -1166,7 +1166,7 @@ learnt today") def rmDockable(self, dock): self.removeDockWidget(dock) - # Card & deck stats + # Stats and graphs ########################################################################## def setupCardStats(self): @@ -1178,29 +1178,8 @@ learnt today") def onDeckStats(self): aqt.stats.deckStats(self) - # Graphs - ########################################################################## - - 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.")) + def onGraphs(self): + aqt.stats.graphs(self) # 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.actionDstats, s, self.onDeckStats) 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.actionAbout, s, self.onAbout) self.connect(m.actionStarthere, s, self.onStartHere) diff --git a/aqt/stats.py b/aqt/stats.py index 3f0f5f775..99f0ade3f 100644 --- a/aqt/stats.py +++ b/aqt/stats.py @@ -58,16 +58,17 @@ class CardStats(object):
%s
"""%txt) -# Deck stats +# Modal dialog that supports dumping to browser (for printing, etc) ###################################################################### class PrintableReport(QDialog): - def __init__(self, mw, type, title, func, css): + def __init__(self, mw, type, title, func, css, extra): self.mw = mw QDialog.__init__(self, mw) restoreGeom(self, type) self.type = type + self.extra = extra self.setWindowTitle(title) self.setModal(True) self.mw.progress.start() @@ -99,10 +100,13 @@ class PrintableReport(QDialog): tmpdir = tempfile.mkdtemp(prefix="anki") path = os.path.join(tmpdir, "report.html") open(path, "w").write(""" -%s""" % ( - self.css, self.report)) +%s%s""" % ( + self.css, self.extra, self.report)) QDesktopServices.openUrl(QUrl("file://" + path)) +# Deck stats +###################################################################### + def deckStats(mw): css=mw.sharedCSS+""" body { margin: 2em; font-family: arial; } @@ -117,3 +121,29 @@ h1 { font-size: 18px; border-bottom: 1px solid #000; margin-top: 1em; _("Deck Statistics"), mw.deck.deckStats, 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 = "" + return PrintableReport( + mw, + "graphs", + _("Graphs"), + lambda: mw.deck.graphs().report(), + css, + buf)