diff --git a/README.md b/README.md index 4ef9ab24..8d273f84 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@ This is a modified version of [obsidian-spaced-repetition](https://github.com/st - setting where to save schedule info by Data Location - save on note file, just as used do. - save on seperate tracked_files.json. - - it still have problems about saving cards shedule info, because when we change note content, the lineNumber and texthash will changes. I add a eventListener, but note work well in some cases. Is there any good idea? - setting convert tracked note to decks - switch Algorithm(only work on saving on seperate tracked_files.json.): Default, anki, [Fsrs](https://github.com/open-spaced-repetition/fsrs.js) - file menu to tracknote/untracknote - show floatbar for reviewing response when reviewing note by click statusbar or review command or sidebar, and can set whether showing the interval or not; - Reviewing a Notes directly [#635]; - when using fsrs, output `ob-revlog.csv`, to optimize the algorithm parameters using [optimizer](https://github.com/open-spaced-repetition/fsrs-optimizer) for better review; +- Statistics: add note view statistics,and today review statistics. ## Maintainers Wanted diff --git a/docs/README_ZH.md b/docs/README_ZH.md index e888cbfb..3cc13713 100644 --- a/docs/README_ZH.md +++ b/docs/README_ZH.md @@ -12,7 +12,8 @@ 4. 在有多个标签时,可不用选标签,直接打开笔记; 5. 算法可以切换:默认的 Anki 优化算法、Anki 算法、[Fsrs 算法](https://github.com/open-spaced-repetition/fsrs.js); 6. 使用 Fsrs 算法时,可根据标签输出重复日志文件 `ob_revlog.csv`,以使用[optimizer](https://github.com/open-spaced-repetition/fsrs-optimizer) 优化算法参数,达到更好的复习效果; -7. 其他待发现的小改动; +7. 数据表中新增笔记复习情况、当天复习情况; +8. 其他待发现的小改动; **注意** 没有使用过 obsidian-spaced-repetition 插件的可以直接用,正在使用 obsidian-spaced-repetition 插件的话,建议试用前先备份 :yum: @@ -41,8 +42,8 @@ github: https://github.com/open-spaced-repetition/obsidian-spaced-repetition-rec 或: -1. 从[最新发布](https://github.com/open-spaced-repetition/obsidian-spaced-repetition-recall/releases/)中下载 main.js, manifest.json, styles.css; -2. 在 `Vault-name/.obsidian/plugins` 中新建个文件夹`obsidian-spaced-repetition-recall`, 把刚下的文件放入新建的文件夹中; +1. 从[最新发布](https://github.com/open-spaced-repetition/obsidian-spaced-repetition-recall/releases/)中下载压缩包(已包括 main.js, manifest.json, styles.css); +2. 在 `Vault-name/.obsidian/plugins` 中,把刚下的文件解压(应是`obsidian-spaced-repetition-recall`文件夹)放入`plugins`文件夹下,; 3. 重启Obsidan 开启插件,就可以使用了。 ## Thanks diff --git a/docs/changelog.md b/docs/changelog.md index d7f9977b..c78e13cf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +## [1.10.1.11] +- 修复: 多设备同步及保存 tracked_files.json 问题; +- 修复: 多标签选择问题 #15 +- 新增: 数据表中可查看笔记复习情况、当天复习情况; + + ## [1.10.1.10] - fix #12, #14, 复习后数据没有同步更新的问题. diff --git a/manifest.json b/manifest.json index f8d57cd6..4c90e40e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-spaced-repetition-recall", "name": "Spaced Repetition Recall", - "version": "1.10.1.10", + "version": "1.10.1.11", "minAppVersion": "0.15.4", "description": "Fight the forgetting curve by reviewing flashcards & entire notes.", "author": "Newdea", diff --git a/package-lock.json b/package-lock.json index 16028e49..7959ba11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-spaced-repetition", - "version": "1.10.1.9", + "version": "1.10.1.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-spaced-repetition", - "version": "1.10.1.9", + "version": "1.10.1.11", "license": "MIT", "dependencies": { "chart.js": "^4.3.3", diff --git a/package.json b/package.json index 11724298..ebbf5bf5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-spaced-repetition", - "version": "1.10.1.10", + "version": "1.10.1.11", "description": "Fight the forgetting curve by reviewing flashcards & entire notes.", "main": "main.js", "scripts": { diff --git a/src/data.ts b/src/data.ts index e30bfcf9..367b7563 100644 --- a/src/data.ts +++ b/src/data.ts @@ -21,7 +21,7 @@ import { } from "./constants"; const ROOT_DATA_PATH = "./tracked_files.json"; -const PLUGIN_DATA_PATH = "./.obsidian/plugins/obsidian-spaced-repetition-recall/tracked_files.json"; +// const PLUGIN_DATA_PATH = "./.obsidian/plugins/obsidian-spaced-repetition-recall/tracked_files.json"; /** * SrsData. @@ -45,6 +45,15 @@ interface SrsData { cardRepeatQueue: number[]; toDayAllQueue: Record; toDayLatterQueue: Record; + + /** + * @type {ReviewedCounts} + */ + reviewedCounts: ReviewedCounts; + /** + * @type {ReviewedCounts} + */ + reviewedCardCounts: ReviewedCounts; /** * @type {RepetitionItem[]} */ @@ -72,6 +81,8 @@ export enum RPITEMTYPE { CARD = "card", } +export type ReviewedCounts = Record; + /** * RepetitionItem. */ @@ -177,6 +188,8 @@ const DEFAULT_SRS_DATA: SrsData = { cardRepeatQueue: [], toDayAllQueue: {}, toDayLatterQueue: {}, + reviewedCounts: {}, + reviewedCardCounts: {}, items: [], trackedFiles: [], lastQueue: 0, @@ -668,6 +681,13 @@ export class DataStore { return null; } + getReviewedCounts() { + return this.data.reviewedCounts; + } + getReviewedCardCounts(): ReviewedCounts { + return this.data.reviewedCardCounts; + } + /** * reviewId. * update data according to response opt @@ -1200,6 +1220,36 @@ export class DataStore { } } + updateReviewedCounts(id: number, type: RPITEMTYPE = RPITEMTYPE.NOTE) { + let rc =this.data.reviewedCounts; + if(type === RPITEMTYPE.NOTE){ + rc = this.data.reviewedCounts; + }else{ + rc = this.data.reviewedCardCounts; + } + // const date = new Date().toLocaleDateString(); + const date = window.moment(new Date()).format("YYYY-MM-DD"); + if (!(date in rc)) { + rc[date] = { due: 0, new: 0 }; + } + if (this.isDue(id)) { + const item = this.getItembyID(id); + if (this.plugin.data.settings.algorithm === algorithmNames.Fsrs) { + const data: FsrsData = item.data as FsrsData; + if (data.scheduled_days >= 1) { + rc[date].due++; + } + } else { + const data: AnkiData = item.data as AnkiData; + if (data.lastInterval >= 1) { + rc[date].due++; + } + } + } else { + rc[date].new++; + } + } + /** * renameTrackedFile. * diff --git a/src/flashcard-modal-algo.tsx b/src/flashcard-modal-algo.tsx index 42ac2232..97b5c6bc 100644 --- a/src/flashcard-modal-algo.tsx +++ b/src/flashcard-modal-algo.tsx @@ -28,7 +28,7 @@ import { import { escapeRegexString, cyrb53 } from "src/utils"; import { t } from "src/lang/helpers"; import { DataLocation, algorithmNames } from "./settings"; -import { RepetitionItem } from "./data"; +import { RPITEMTYPE, RepetitionItem } from "./data"; export enum FlashcardModalMode { DecksList, @@ -310,7 +310,7 @@ export class FlashcardModal extends Modal { setIcon(this.openNoteFileButton, "file-edit"); this.openNoteFileButton.setAttribute("aria-label", t("OPEN_NOTE")); this.openNoteFileButton.addEventListener("click", async () => { - const activeLeaf: WorkspaceLeaf = this.plugin.app.workspace.getLeaf("tab"); + const activeLeaf: WorkspaceLeaf = this.plugin.app.workspace.getLeaf(); await activeLeaf.openFile(this.currentCard.note); if (activeLeaf.view instanceof MarkdownView) { const activeView = activeLeaf.view; @@ -620,6 +620,7 @@ export class FlashcardModal extends Modal { ]); } const id = cardinfo.itemIds[this.currentCard.siblingIdx]; + store.updateReviewedCounts(id, RPITEMTYPE.CARD); store.reviewId(id, opt); store.save(); } diff --git a/src/lang/locale/ar.ts b/src/lang/locale/ar.ts index 962e86b7..6875befb 100644 --- a/src/lang/locale/ar.ts +++ b/src/lang/locale/ar.ts @@ -204,4 +204,9 @@ export default { CARD_TYPE_YOUNG: "صغيرة", CARD_TYPE_MATURE: "ناضجة", CARD_TYPES_SUMMARY: " ${totalCardsCount} :إجمالي عدد البطاقات", + REVIEWED_TODAY: "Reviewed today", + REVIEWED_TODAY_DESC: "counts of cards/notes you have reviewed today", + NEW_LEARNED: "New Learned", + DUE_REVIEWED: "due Reviewed", + REVIEWED_TODAY_SUMMARY: "Total Reviewed today: ${totalreviewedCount}", }; diff --git a/src/lang/locale/cz.ts b/src/lang/locale/cz.ts index df82be5d..dbfb550c 100644 --- a/src/lang/locale/cz.ts +++ b/src/lang/locale/cz.ts @@ -208,4 +208,9 @@ export default { CARD_TYPE_YOUNG: "Mladá", CARD_TYPE_MATURE: "Dospělá", CARD_TYPES_SUMMARY: "Kartiček celkem: ${totalCardsCount}", + REVIEWED_TODAY: "Reviewed today", + REVIEWED_TODAY_DESC: "counts of cards/notes you have reviewed today", + NEW_LEARNED: "New Learned", + DUE_REVIEWED: "due Reviewed", + REVIEWED_TODAY_SUMMARY: "Total Reviewed today: ${totalreviewedCount}", }; diff --git a/src/lang/locale/de.ts b/src/lang/locale/de.ts index ed71f2f9..2fb2a2ea 100644 --- a/src/lang/locale/de.ts +++ b/src/lang/locale/de.ts @@ -219,4 +219,9 @@ export default { CARD_TYPE_YOUNG: "Jung", CARD_TYPE_MATURE: "Ausgereift", CARD_TYPES_SUMMARY: "Insgesamt ${totalCardsCount} Karten", + REVIEWED_TODAY: "Reviewed today", + REVIEWED_TODAY_DESC: "counts of cards/notes you have reviewed today", + NEW_LEARNED: "New Learned", + DUE_REVIEWED: "due Reviewed", + REVIEWED_TODAY_SUMMARY: "Total Reviewed today: ${totalreviewedCount}", }; diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 0c7ac7a3..c773af4b 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -208,4 +208,9 @@ export default { CARD_TYPE_YOUNG: "Young", CARD_TYPE_MATURE: "Mature", CARD_TYPES_SUMMARY: "Total cards: ${totalCardsCount}", + REVIEWED_TODAY: "Reviewed today", + REVIEWED_TODAY_DESC: "counts of cards/notes you have reviewed today", + NEW_LEARNED: "New Learned", + DUE_REVIEWED: "due Reviewed", + REVIEWED_TODAY_SUMMARY: "Total Reviewed today: ${totalreviewedCount}", }; diff --git a/src/lang/locale/es.ts b/src/lang/locale/es.ts index fcaccab1..88bf375c 100644 --- a/src/lang/locale/es.ts +++ b/src/lang/locale/es.ts @@ -213,4 +213,9 @@ export default { CARD_TYPE_YOUNG: "Joven", CARD_TYPE_MATURE: "Madura", CARD_TYPES_SUMMARY: "Tarjetas Totales: ${totalCardsCount}", + REVIEWED_TODAY: "Reviewed today", + REVIEWED_TODAY_DESC: "counts of cards/notes you have reviewed today", + NEW_LEARNED: "New Learned", + DUE_REVIEWED: "due Reviewed", + REVIEWED_TODAY_SUMMARY: "Total Reviewed today: ${totalreviewedCount}", }; diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts index 02f3e5c7..0d44c094 100644 --- a/src/lang/locale/ja.ts +++ b/src/lang/locale/ja.ts @@ -212,4 +212,9 @@ export default { CARD_TYPE_YOUNG: "復習(初期)", CARD_TYPE_MATURE: "復習(後期)", CARD_TYPES_SUMMARY: "カードの合計: ${totalCardsCount}枚", + REVIEWED_TODAY: "Reviewed today", + REVIEWED_TODAY_DESC: "counts of cards/notes you have reviewed today", + NEW_LEARNED: "New Learned", + DUE_REVIEWED: "due Reviewed", + REVIEWED_TODAY_SUMMARY: "Total Reviewed today: ${totalreviewedCount}", }; diff --git a/src/lang/locale/ko.ts b/src/lang/locale/ko.ts index 948bff28..9eb13cd8 100644 --- a/src/lang/locale/ko.ts +++ b/src/lang/locale/ko.ts @@ -209,4 +209,9 @@ export default { CARD_TYPE_YOUNG: "Young", CARD_TYPE_MATURE: "Mature", CARD_TYPES_SUMMARY: "전체 카드 수: ${totalCardsCount}", + REVIEWED_TODAY: "Reviewed today", + REVIEWED_TODAY_DESC: "counts of cards/notes you have reviewed today", + NEW_LEARNED: "New Learned", + DUE_REVIEWED: "due Reviewed", + REVIEWED_TODAY_SUMMARY: "Total Reviewed today: ${totalreviewedCount}", }; diff --git a/src/lang/locale/pt-br.ts b/src/lang/locale/pt-br.ts index f96c2415..cea2608d 100644 --- a/src/lang/locale/pt-br.ts +++ b/src/lang/locale/pt-br.ts @@ -215,4 +215,9 @@ export default { CARD_TYPE_YOUNG: "Jovem", CARD_TYPE_MATURE: "Amadurecido", CARD_TYPES_SUMMARY: "Total de cartas: ${totalCardsCount}", + REVIEWED_TODAY: "Reviewed today", + REVIEWED_TODAY_DESC: "counts of cards/notes you have reviewed today", + NEW_LEARNED: "New Learned", + DUE_REVIEWED: "due Reviewed", + REVIEWED_TODAY_SUMMARY: "Total Reviewed today: ${totalreviewedCount}", }; diff --git a/src/lang/locale/ru.ts b/src/lang/locale/ru.ts index 0d9b86ee..0f19982f 100644 --- a/src/lang/locale/ru.ts +++ b/src/lang/locale/ru.ts @@ -219,4 +219,9 @@ export default { CARD_TYPE_YOUNG: "Молодых", CARD_TYPE_MATURE: "Взрослых", CARD_TYPES_SUMMARY: "Всего карточек: ${totalCardsCount}", + REVIEWED_TODAY: "Reviewed today", + REVIEWED_TODAY_DESC: "counts of cards/notes you have reviewed today", + NEW_LEARNED: "New Learned", + DUE_REVIEWED: "due Reviewed", + REVIEWED_TODAY_SUMMARY: "Total Reviewed today: ${totalreviewedCount}", }; diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 371ba2de..1e4f0c5f 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -193,4 +193,9 @@ export default { CARD_TYPE_YOUNG: "较新", CARD_TYPE_MATURE: "熟悉", CARD_TYPES_SUMMARY: "总卡片数: ${totalCardsCount}", + REVIEWED_TODAY: "今天复习情况", + REVIEWED_TODAY_DESC: "今天已经复习的卡片/笔记的数量", + NEW_LEARNED: "新学", + DUE_REVIEWED: "复习", + REVIEWED_TODAY_SUMMARY: "总复习数: ${totalreviewedCount}", }; diff --git a/src/lang/locale/zh-tw.ts b/src/lang/locale/zh-tw.ts index 3b0a4cd7..43574048 100644 --- a/src/lang/locale/zh-tw.ts +++ b/src/lang/locale/zh-tw.ts @@ -192,4 +192,9 @@ export default { CARD_TYPE_YOUNG: "較新", CARD_TYPE_MATURE: "熟悉", CARD_TYPES_SUMMARY: "總卡片數: ${totalCardsCount}", + REVIEWED_TODAY: "今天復習情況", + REVIEWED_TODAY_DESC: "今天已經復習的卡片/筆記的數量", + NEW_LEARNED: "新學", + DUE_REVIEWED: "復習", + REVIEWED_TODAY_SUMMARY: "總復習數: ${totalreviewedCount}", }; diff --git a/src/main.ts b/src/main.ts index 5201d5ce..de5f3fe8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -92,6 +92,7 @@ export default class SRPlugin extends Plugin { public deckTree: Deck = new Deck("root", null); public dueDatesFlashcards: Record = {}; // Record<# of days in future, due count> public cardStats: Stats; + public noteStats: Stats; // https://github.com/martin-jw/obsidian-recall/blob/main/src/main.ts public store: DataStore; @@ -578,6 +579,14 @@ export default class SRPlugin extends Plugin { matureCount: 0, }; + this.noteStats = { + eases: {}, + intervals: {}, + newCount: 0, + youngCount: 0, + matureCount: 0, + }; + // check trackfile store.reLoad(); @@ -646,8 +655,17 @@ export default class SRPlugin extends Plugin { // update single note deck data, only tagged reviewnote if (!this.store.isTracked(note.path)) { this.store.trackFile(note.path, deckname); + this.noteStats.newCount++; + } else { + const id = store.getFileId(note.path); + const scheduling = store.getSchedbyId(id); + if (scheduling != null) { + this.updateStats(this.noteStats, scheduling); + } else { + this.noteStats.newCount++; + } } - this.store.syncRCDataToSRrevDeck(this.reviewDecks[deckname], note); + store.syncRCDataToSRrevDeck(this.reviewDecks[deckname], note); const id = store.getFileId(note.path); const settings = this.data.settings; @@ -913,6 +931,7 @@ export default class SRPlugin extends Plugin { } } + store.updateReviewedCounts(fileId); store.reviewId(fileId, response); store.save(); @@ -1295,22 +1314,10 @@ export default class SRPlugin extends Plugin { const interval: number = parseInt(scheduling[i][2]), ease: number = parseInt(scheduling[i][3]); - if (!Object.prototype.hasOwnProperty.call(this.cardStats.intervals, interval)) { - this.cardStats.intervals[interval] = 0; - } - this.cardStats.intervals[interval]++; - if (!Object.prototype.hasOwnProperty.call(this.cardStats.eases, ease)) { - this.cardStats.eases[ease] = 0; - } - this.cardStats.eases[ease]++; totalNoteEase += ease; scheduledCount++; - if (interval >= 32) { - this.cardStats.matureCount++; - } else { - this.cardStats.youngCount++; - } + this.updateStats(this.cardStats, scheduling[i]); if (this.data.buryList.includes(cardTextHash)) { this.deckTree.countFlashcard([...deckPath]); @@ -1533,6 +1540,25 @@ export default class SRPlugin extends Plugin { } } + updateStats(stats: Stats, scheduling: RegExpMatchArray) { + const interval: number = parseInt(scheduling[2]), + ease: number = parseInt(scheduling[3]); + if (!Object.prototype.hasOwnProperty.call(stats.intervals, interval)) { + stats.intervals[interval] = 0; + } + stats.intervals[interval]++; + if (!Object.prototype.hasOwnProperty.call(stats.eases, ease)) { + stats.eases[ease] = 0; + } + stats.eases[ease]++; + + if (interval >= 32) { + stats.matureCount++; + } else { + stats.youngCount++; + } + } + updateStatusBar() { this.statusBar.setText( t("STATUS_BAR", { diff --git a/src/stats-modal.tsx b/src/stats-modal.tsx index b635a26c..c075d2ad 100644 --- a/src/stats-modal.tsx +++ b/src/stats-modal.tsx @@ -20,6 +20,9 @@ import type SRPlugin from "src/main"; import { getKeysPreserveType, getTypedObjectEntries } from "src/utils"; import { textInterval } from "src/scheduling"; import { t } from "src/lang/helpers"; +import { RPITEMTYPE } from "./data"; +import { State } from "fsrs.js"; +import { algorithmNames } from "./settings"; Chart.register( BarElement, @@ -53,14 +56,22 @@ export class StatsModal extends Modal { this.titleEl.setText(`${t("STATS_TITLE")} `); this.titleEl.addClass("sr-centered"); this.titleEl.innerHTML += ( - +
+ + +
); this.modalEl.style.height = "100%"; @@ -75,6 +86,78 @@ export class StatsModal extends Modal { const { contentEl } = this; contentEl.style.textAlign = "center"; + contentEl.innerHTML += ( +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ ); + + const chartTypeEl = document.getElementById("sr-chart-type") as HTMLSelectElement; + chartTypeEl.addEventListener("click", () => { + const chartType = chartTypeEl.value; + if (chartType === RPITEMTYPE.NOTE) { + this.createNoteStatsChart(); + return; + } else { + this.createCardStatsChart(); + return; + } + }); + + this.createCardStatsChart(); + } + + onClose(): void { + const { contentEl } = this; + contentEl.empty(); + } + + private createCardStatsChart() { + //Add today review data + const rc = this.plugin.store.getReviewedCardCounts(); + const now = window.moment(Date.now()); + const todayDate: string = now.format("YYYY-MM-DD"); + if(!(todayDate in rc)){ + rc[todayDate]={due: 0, new: 0}; + } + const rdueCnt = rc[todayDate].due, + rnewCnt = rc[todayDate].new; + + const totalreviewedCount = rdueCnt + rnewCnt; + createStatsChart( + "bar", + "todayReviewedChart", + t("REVIEWED_TODAY"), + t("REVIEWED_TODAY_DESC"), + [ + `${t("NEW_LEARNED")} - ${Math.round(rnewCnt)}`, + `${t("DUE_REVIEWED")} - ${Math.round(rdueCnt)}`, + ], + [rnewCnt, rdueCnt], + t("REVIEWED_TODAY_SUMMARY", { totalreviewedCount }), + t("COUNT"), + "", + t("NUMBER_OF_CARDS"), + ); + // Add forecast let maxN: number = Math.max(...getKeysPreserveType(this.plugin.dueDatesFlashcards)); for (let dueOffset = 0; dueOffset <= maxN; dueOffset++) { @@ -96,26 +179,6 @@ export class StatsModal extends Modal { const scheduledCount: number = cardStats.youngCount + cardStats.matureCount; maxN = Math.max(maxN, 1); - contentEl.innerHTML += ( -
- - -
-
- - -
-
- - -
-
- -
- -
- ); - createStatsChart( "bar", "forecastChart", @@ -179,12 +242,23 @@ export class StatsModal extends Modal { .reduce((a, b) => a + b, 0) / scheduledCount, ) || 0; + const esaeStr: string[] = []; + Object.keys(cardStats.eases).forEach((value: string) => { + const v = parseInt(value); + if (this.plugin.data.settings.algorithm === algorithmNames.Fsrs) { + esaeStr.push(`${State[v]} `); + } else { + esaeStr.push(`${v} `); + } + }); + createStatsChart( "bar", "easesChart", t("EASES"), "", - Object.keys(cardStats.eases), + esaeStr, + // Object.keys(cardStats.eases), Object.values(cardStats.eases), t("EASES_SUMMARY", { avgEase: average_ease }), t("COUNT"), @@ -215,9 +289,164 @@ export class StatsModal extends Modal { ); } - onClose(): void { - const { contentEl } = this; - contentEl.empty(); + private createNoteStatsChart() { + //Add today review data + const rc = this.plugin.store.getReviewedCounts(); + const now = window.moment(Date.now()); + const todayDate: string = now.format("YYYY-MM-DD"); + if(!(todayDate in rc)){ + rc[todayDate]={due: 0, new: 0}; + } + const rdueCnt = rc[todayDate].due, + rnewCnt = rc[todayDate].new; + + const totalreviewedCount = rdueCnt + rnewCnt; + createStatsChart( + "bar", + "todayReviewedChart", + t("REVIEWED_TODAY"), + t("REVIEWED_TODAY_DESC"), + [ + `${t("NEW_LEARNED")} - ${Math.round(rnewCnt)}`, + `${t("DUE_REVIEWED")} - ${Math.round(rdueCnt)}`, + ], + [rnewCnt, rdueCnt], + t("REVIEWED_TODAY_SUMMARY", { totalreviewedCount }), + t("COUNT"), + "", + t("NUMBER_OF_CARDS"), + ); + + // Add forecast + let maxN: number = Math.max(...getKeysPreserveType(this.plugin.dueDatesNotes)); + for (let dueOffset = 0; dueOffset <= maxN; dueOffset++) { + if (!Object.prototype.hasOwnProperty.call(this.plugin.dueDatesNotes, dueOffset)) { + this.plugin.dueDatesNotes[dueOffset] = 0; + } + } + + const dueDatesFlashcardsCopy: Record = { 0: 0 }; + for (const [dueOffset, dueCount] of getTypedObjectEntries(this.plugin.dueDatesNotes)) { + if (dueOffset <= 0) { + dueDatesFlashcardsCopy[0] += dueCount; + } else { + dueDatesFlashcardsCopy[dueOffset] = dueCount; + } + } + + const cardStats: Stats = this.plugin.noteStats; + const scheduledCount: number = cardStats.youngCount + cardStats.matureCount; + maxN = Math.max(maxN, 1); + + createStatsChart( + "bar", + "forecastChart", + t("FORECAST"), + t("FORECAST_DESC"), + Object.keys(dueDatesFlashcardsCopy), + Object.values(dueDatesFlashcardsCopy), + t("REVIEWS_PER_DAY", { avg: (scheduledCount / maxN).toFixed(1) }), + t("SCHEDULED"), + t("DAYS"), + t("NUMBER_OF_CARDS"), + ); + + maxN = Math.max(...getKeysPreserveType(cardStats.intervals)); + for (let interval = 0; interval <= maxN; interval++) { + if (!Object.prototype.hasOwnProperty.call(cardStats.intervals, interval)) { + cardStats.intervals[interval] = 0; + } + } + + // Add intervals + const average_interval: string = textInterval( + Math.round( + (getTypedObjectEntries(cardStats.intervals) + .map(([interval, count]) => interval * count) + .reduce((a, b) => a + b, 0) / + scheduledCount) * + 10, + ) / 10 || 0, + false, + ), + longest_interval: string = textInterval( + Math.max(...getKeysPreserveType(cardStats.intervals)) || 0, + false, + ); + + createStatsChart( + "bar", + "intervalsChart", + t("INTERVALS"), + t("INTERVALS_DESC"), + Object.keys(cardStats.intervals), + Object.values(cardStats.intervals), + t("INTERVALS_SUMMARY", { avg: average_interval, longest: longest_interval }), + t("COUNT"), + t("DAYS"), + t("NUMBER_OF_CARDS"), + ); + + // Add eases + const eases: number[] = getKeysPreserveType(cardStats.eases); + for (let ease = Math.min(...eases); ease <= Math.max(...eases); ease++) { + if (!Object.prototype.hasOwnProperty.call(cardStats.eases, ease)) { + cardStats.eases[ease] = 0; + } + } + const average_ease: number = + Math.round( + getTypedObjectEntries(cardStats.eases) + .map(([ease, count]) => ease * count) + .reduce((a, b) => a + b, 0) / scheduledCount, + ) || 0; + + const esaeStr: string[] = []; + Object.keys(cardStats.eases).forEach((value: string) => { + const v = parseInt(value); + if (this.plugin.data.settings.algorithm === algorithmNames.Fsrs) { + esaeStr.push(`${State[v]} `); + } else { + esaeStr.push(`${v} `); + } + }); + + createStatsChart( + "bar", + "easesChart", + t("EASES"), + "", + esaeStr, + // Object.keys(cardStats.eases), + Object.values(cardStats.eases), + t("EASES_SUMMARY", { avgEase: average_ease }), + t("COUNT"), + t("EASES"), + t("NUMBER_OF_CARDS"), + ); + + // Add card types + const totalCardsCount: number = this.plugin.dueNotesCount + this.plugin.newNotesCount; + createStatsChart( + "pie", + "cardTypesChart", + t("CARD_TYPES"), + "", + // t("CARD_TYPES_DESC"), + [ + `${t("CARD_TYPE_NEW")} - ${Math.round( + (cardStats.newCount / totalCardsCount) * 100, + )}%`, + `${t("CARD_TYPE_YOUNG")} - ${Math.round( + (cardStats.youngCount / totalCardsCount) * 100, + )}%`, + `${t("CARD_TYPE_MATURE")} - ${Math.round( + (cardStats.matureCount / totalCardsCount) * 100, + )}%`, + ], + [cardStats.newCount, cardStats.youngCount, cardStats.matureCount], + t("CARD_TYPES_SUMMARY", { totalCardsCount }), + ); } } @@ -261,6 +490,13 @@ function createStatsChart( const shouldFilter = canvasId === "forecastChart" || canvasId === "intervalsChart"; + const statsE1 = document.getElementById(canvasId) as HTMLCanvasElement; + const existingChart = Chart.getChart(statsE1); + if (existingChart) { + existingChart.unbindEvents(); + existingChart.destroy(); + } + const statsChart = new Chart(document.getElementById(canvasId) as HTMLCanvasElement, { type, data: { @@ -304,31 +540,38 @@ function createStatsChart( if (shouldFilter) { const chartPeriodEl = document.getElementById("sr-chart-period") as HTMLSelectElement; chartPeriodEl.addEventListener("click", () => { - let filteredLabels, filteredData; - const chartPeriod = chartPeriodEl.value; - if (chartPeriod === "month") { - filteredLabels = labels.slice(0, 31); - filteredData = data.slice(0, 31); - } else if (chartPeriod === "quarter") { - filteredLabels = labels.slice(0, 91); - filteredData = data.slice(0, 91); - } else if (chartPeriod === "year") { - filteredLabels = labels.slice(0, 366); - filteredData = data.slice(0, 366); - } else { - filteredLabels = labels; - filteredData = data; + if (statsChart.canvas != null) { + chartPeriodCallBack(chartPeriodEl); } - - statsChart.data.labels = filteredLabels; - statsChart.data.datasets[0] = { - label: seriesTitle, - backgroundColor, - data: filteredData, - }; - statsChart.update(); }); + chartPeriodCallBack(chartPeriodEl); } document.getElementById(`${canvasId}Summary`).innerText = summary; + + function chartPeriodCallBack(chartPeriodEl: HTMLSelectElement) { + let filteredLabels, filteredData; + const chartPeriod = chartPeriodEl.value; + if (chartPeriod === "month") { + filteredLabels = labels.slice(0, 31); + filteredData = data.slice(0, 31); + } else if (chartPeriod === "quarter") { + filteredLabels = labels.slice(0, 91); + filteredData = data.slice(0, 91); + } else if (chartPeriod === "year") { + filteredLabels = labels.slice(0, 366); + filteredData = data.slice(0, 366); + } else { + filteredLabels = labels; + filteredData = data; + } + + statsChart.data.labels = filteredLabels; + statsChart.data.datasets[0] = { + label: seriesTitle, + backgroundColor, + data: filteredData, + }; + statsChart.update(); + } }