From 329186f140f5bb784dde3bf2b12fbfed99477678 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 26 Jun 2020 10:42:10 +1000 Subject: [PATCH] qt's js code now shares ts/node_modules; more graph work --- Makefile | 10 +- proto/backend.proto | 8 +- qt/Makefile | 6 +- qt/aqt/mediasrv.py | 7 +- qt/ts/Makefile | 34 +++++ qt/ts/package-lock.json | 185 --------------------------- qt/ts/package.json | 21 ---- qt/ts/tsconfig.json | 36 +++--- rslib/src/stats/graphs.rs | 99 +-------------- ts/Makefile | 12 +- ts/package.json | 8 ++ ts/src/stats/AddedGraph.svelte | 6 +- ts/src/stats/ButtonsGraph.svelte | 32 +++++ ts/src/stats/CardCounts.svelte | 17 +++ ts/src/stats/EaseGraph.svelte | 6 +- ts/src/stats/GraphsPage.svelte | 16 ++- ts/src/stats/IntervalsGraph.svelte | 6 +- ts/src/stats/TodayStats.svelte | 17 +++ ts/src/stats/buttons.ts | 195 +++++++++++++++++++++++++++++ ts/src/stats/card-counts.ts | 60 +++++++++ ts/src/stats/hours.ts | 41 ++++++ ts/src/stats/today.ts | 83 ++++++++++++ 22 files changed, 556 insertions(+), 349 deletions(-) create mode 100644 qt/ts/Makefile delete mode 100644 qt/ts/package-lock.json delete mode 100644 qt/ts/package.json create mode 100644 ts/src/stats/ButtonsGraph.svelte create mode 100644 ts/src/stats/CardCounts.svelte create mode 100644 ts/src/stats/TodayStats.svelte create mode 100644 ts/src/stats/buttons.ts create mode 100644 ts/src/stats/card-counts.ts create mode 100644 ts/src/stats/hours.ts create mode 100644 ts/src/stats/today.ts diff --git a/Makefile b/Makefile index 672a757a5..301bca7f9 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ DEVFLAGS := $(BUILDFLAGS) RUNFLAGS := CHECKABLE_PY := pylib qt CHECKABLE_RS := rslib rspy -DEVEL := rslib rspy pylib qt +DEVEL := rslib rspy pylib ts qt .PHONY: all all: run @@ -115,10 +115,14 @@ qt/po/repo: $(MAKE) pull-i18n .PHONY: build -build: clean-dist build-rspy build-pylib build-qt add-buildhash +build: clean-dist build-ts build-rspy build-pylib build-qt add-buildhash @echo @echo "Build complete." +.PHONY: build-ts +build-ts: + $(SUBMAKE) -C ts build + .PHONY: build-rspy build-rspy: pyenv buildhash @set -eu -o pipefail ${SHELLFLAGS}; \ @@ -154,7 +158,7 @@ check: pyenv buildhash prepare @set -eu -o pipefail ${SHELLFLAGS}; \ .github/scripts/trailing-newlines.sh; \ . "${ACTIVATE_SCRIPT}"; \ - for dir in $(CHECKABLE_RS) $(CHECKABLE_PY); do \ + for dir in $(CHECKABLE_RS) ts $(CHECKABLE_PY); do \ $(SUBMAKE) -C $$dir check; \ done; @echo diff --git a/proto/backend.proto b/proto/backend.proto index 552a499d9..b806ceb41 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -1037,6 +1037,12 @@ message ButtonsGraphData { } message RevlogEntry { + enum ReviewKind { + LEARNING = 0; + REVIEW = 1; + RELEARNING = 2; + EARLY_REVIEW = 4; + } int64 id = 1; int64 cid = 2; int32 usn = 3; @@ -1045,5 +1051,5 @@ message RevlogEntry { int32 last_interval = 6; uint32 ease_factor = 7; uint32 taken_millis = 8; - uint32 review_kind = 9; + ReviewKind review_kind = 9; } diff --git a/qt/Makefile b/qt/Makefile index 9193df37d..c6d367064 100644 --- a/qt/Makefile +++ b/qt/Makefile @@ -55,7 +55,7 @@ all: check TSDEPS := $(wildcard ts/src/*.ts) $(wildcard ts/scss/*.scss) .build/js: $(TSDEPS) - (cd ts && npm i && npm run build) + (cd ts && make build) python ./tools/extract_scss_colors.py @touch $@ @@ -81,7 +81,7 @@ check: .build/pyaudio $(BUILD_STEPS) .build/mypy .build/test .build/fmt .build/i fix: $(BUILD_STEPS) isort $(ISORTARGS) python -m black $(BLACKARGS) - (cd ts && npm run pretty) + (cd ts && make fix) .PHONY: clean clean: @@ -93,7 +93,7 @@ clean: JSDEPS := $(patsubst ts/src/%.ts, web/%.js, $(TSDEPS)) .build/ts-fmt: $(TSDEPS) - (cd ts && npm i && npm run check-pretty) + (cd ts && make check) @touch $@ # Checking python diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 66fb2ecb9..d0b562a5a 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -182,7 +182,7 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): cmd = self.path[len("/_anki/") :] if cmd == "graphData": - content_length = int(self.headers['Content-Length']) + content_length = int(self.headers["Content-Length"]) body = self.rfile.read(content_length) data = graph_data(self.mw.col, **from_json_bytes(body)) else: @@ -198,13 +198,14 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): self.wfile.write(data) -def graph_data(col: Collection, search: str, days: str) -> bytes: +def graph_data(col: Collection, search: str, days: int) -> bytes: try: return col.backend.graphs(search=search, days=days) except Exception as e: # likely searching error print(e) - return b'' + return b"" + # work around Windows machines with incorrect mime type RequestHandler.extensions_map[".css"] = "text/css" diff --git a/qt/ts/Makefile b/qt/ts/Makefile new file mode 100644 index 000000000..2883b87ca --- /dev/null +++ b/qt/ts/Makefile @@ -0,0 +1,34 @@ +SHELL := /bin/bash + +ifndef SHELLFLAGS + SHELLFLAGS := +endif + +.SHELLFLAGS := -eu -o pipefail ${SHELLFLAGS} -c +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules + +ifndef OS + OS := unknown +endif + +.DELETE_ON_ERROR: +.SUFFIXES: + +BIN := ../../ts/node_modules/.bin + +PHONY: all +all: check + +PHONY: build +build: + $(BIN)/tsc --build + $(BIN)/sass --no-source-map scss:../aqt_data/web + +.PHONY: check +check: + $(BIN)/prettier --check src/*.ts + +.PHONY: fix +fix: + $(BIN)/prettier --write src/*.ts diff --git a/qt/ts/package-lock.json b/qt/ts/package-lock.json deleted file mode 100644 index 51f6a62dc..000000000 --- a/qt/ts/package-lock.json +++ /dev/null @@ -1,185 +0,0 @@ -{ - "name": "anki-dtop-js", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@types/jquery": { - "version": "3.3.31", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz", - "integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==", - "dev": true, - "requires": { - "@types/sizzle": "*" - } - }, - "@types/jqueryui": { - "version": "1.12.9", - "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.9.tgz", - "integrity": "sha512-bHE7BiG+5Sviy/eA9Npz5HHF3hv40XjaEbpYtSJPaNwuyxhSJ0qWlE8C5DgNMfobVOZ2aSTrM1iGDCGmvlbxOg==", - "dev": true, - "requires": { - "@types/jquery": "*" - } - }, - "@types/mathjax": { - "version": "0.0.36", - "resolved": "https://registry.npmjs.org/@types/mathjax/-/mathjax-0.0.36.tgz", - "integrity": "sha512-TqDJc2GWuTqd/m+G/FbNkN+/TF2OCCHvcawmhIrUaZkdVquMdNZmNiNUkupNg9qctorXXkVLVSogZv1DhmgLmg==", - "dev": true - }, - "@types/sizzle": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", - "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", - "dev": true - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chokidar": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", - "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.3.0" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", - "dev": true, - "optional": true - }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "picomatch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", - "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", - "dev": true - }, - "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true - }, - "readdirp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", - "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", - "dev": true, - "requires": { - "picomatch": "^2.0.7" - } - }, - "sass": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.25.0.tgz", - "integrity": "sha512-uQMjye0Y70SEDGO56n0j91tauqS9E1BmpKHtiYNQScXDHeaE9uHwNEqQNFf4Bes/3DHMNinB6u79JsG10XWNyw==", - "dev": true, - "requires": { - "chokidar": ">=2.0.0 <4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "typescript": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", - "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==", - "dev": true - } - } -} diff --git a/qt/ts/package.json b/qt/ts/package.json deleted file mode 100644 index f0c29ab8d..000000000 --- a/qt/ts/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "anki-dtop-js", - "version": "1.0.0", - "description": "Anki desktop js support files", - "scripts": { - "build": "tsc --build && sass --no-source-map scss:../aqt_data/web", - "pretty": "prettier --write src/*.ts", - "check-pretty": "prettier --check src/*.ts" - }, - "private": true, - "author": "Ankitects Pty Ltd", - "license": "AGPL-3.0-or-later", - "devDependencies": { - "@types/jquery": "^3.3.31", - "@types/jqueryui": "^1.12.9", - "@types/mathjax": "0.0.36", - "prettier": "^1.19.1", - "sass": "^1.25.0", - "typescript": "^3.7.3" - } -} diff --git a/qt/ts/tsconfig.json b/qt/ts/tsconfig.json index e24928bc6..b5772dfff 100644 --- a/qt/ts/tsconfig.json +++ b/qt/ts/tsconfig.json @@ -1,21 +1,19 @@ { - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "lib": [ - "es6", - "dom" - ], - "rootDir": "src", - "outDir": "../aqt_data/web", - "strict": true, - /* Enable all strict type-checking options. */ - "noImplicitAny": false, - /* Raise error on expressions and declarations with an implied 'any' type. */ - "strictNullChecks": false, - /* Enable strict null checks. */ - "noImplicitThis": false, - /* Raise error on 'this' expressions with an implied 'any' type. */ - "esModuleInterop": true - } + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "lib": ["es6", "dom"], + "rootDir": "src", + "outDir": "../aqt_data/web", + "typeRoots": ["../../ts/node_modules/@types"], + "strict": true, + /* Enable all strict type-checking options. */ + "noImplicitAny": false, + /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": false, + /* Enable strict null checks. */ + "noImplicitThis": false, + /* Raise error on 'this' expressions with an implied 'any' type. */ + "esModuleInterop": true + } } diff --git a/rslib/src/stats/graphs.rs b/rslib/src/stats/graphs.rs index 738154f38..046ae0cc4 100644 --- a/rslib/src/stats/graphs.rs +++ b/rslib/src/stats/graphs.rs @@ -3,103 +3,6 @@ use crate::{backend_proto as pb, prelude::*, revlog::RevlogEntry}; -// impl GraphsContext { -// fn observe_button_stats_for_review(&mut self, review: &RevlogEntry) { -// let mut button_num = review.button_chosen as usize; -// if button_num == 0 { -// return; -// } - -// let buttons = &mut self.stats.buttons; -// let category = match review.review_kind { -// RevlogReviewKind::Learning | RevlogReviewKind::Relearning => { -// // V1 scheduler only had 3 buttons in learning -// if button_num == 4 && self.scheduler == SchedulerVersion::V1 { -// button_num = 3; -// } - -// &mut buttons.learn -// } -// RevlogReviewKind::Review | RevlogReviewKind::EarlyReview => { -// if review.last_interval < 21 { -// &mut buttons.young -// } else { -// &mut buttons.mature -// } -// } -// }; - -// if let Some(count) = category.get_mut(button_num - 1) { -// *count += 1; -// } -// } - -// fn observe_hour_stats_for_review(&mut self, review: &RevlogEntry) { -// match review.review_kind { -// RevlogReviewKind::Learning -// | RevlogReviewKind::Review -// | RevlogReviewKind::Relearning => { -// let hour_idx = (((review.id.0 / 1000) + self.local_offset_secs) / 3600) % 24; -// let hour = &mut self.stats.hours[hour_idx as usize]; - -// hour.review_count += 1; -// if review.button_chosen != 1 { -// hour.correct_count += 1; -// } -// } -// RevlogReviewKind::EarlyReview => {} -// } -// } - -// fn observe_today_stats_for_review(&mut self, review: &RevlogEntry) { -// if review.id.0 < self.today_rolled_over_at_millis { -// return; -// } - -// let today = &mut self.stats.today; - -// // total -// today.answer_count += 1; -// today.answer_millis += review.taken_millis; - -// // correct -// if review.button_chosen > 1 { -// today.correct_count += 1; -// } - -// // mature -// if review.last_interval >= 21 { -// today.mature_count += 1; -// if review.button_chosen > 1 { -// today.mature_correct += 1; -// } -// } - -// // type counts -// match review.review_kind { -// RevlogReviewKind::Learning => today.learn_count += 1, -// RevlogReviewKind::Review => today.review_count += 1, -// RevlogReviewKind::Relearning => today.relearn_count += 1, -// RevlogReviewKind::EarlyReview => today.early_review_count += 1, -// } -// } - -// fn observe_card_stats_for_card(&mut self, card: &Card) { -// counts by type -// match card.queue { -// CardQueue::New => cstats.new_count += 1, -// CardQueue::Review if card.ivl >= 21 => cstats.mature_count += 1, -// CardQueue::Review | CardQueue::Learn | CardQueue::DayLearn => { -// cstats.young_or_learning_count += 1 -// } -// CardQueue::Suspended | CardQueue::UserBuried | CardQueue::SchedBuried => { -// cstats.suspended_or_buried_count += 1 -// } -// CardQueue::PreviewRepeat => {} -// } -// } -// } - impl Collection { pub(crate) fn graph_data_for_search( &mut self, @@ -155,7 +58,7 @@ impl From for pb::RevlogEntry { last_interval: e.last_interval, ease_factor: e.ease_factor, taken_millis: e.taken_millis, - review_kind: e.review_kind as u32, + review_kind: e.review_kind as i32, } } } diff --git a/ts/Makefile b/ts/Makefile index 69e812d1c..657aeeac3 100644 --- a/ts/Makefile +++ b/ts/Makefile @@ -38,14 +38,22 @@ dev: .build/proto PHONY: build build: .build/build +PHONY: develop +develop: .build/build + .build/build: .build/proto $(BUILDDEPS) $(wildcard src/*/*.svelte src/*/*.ts) + npm i npm run build @touch $@ .PHONY: check -check: +check: .build/build npm run check .PHONY: fix -fix: +fix: .build/build npm run fix + +.PHONY: clean +clean: + rm -rf .build node_modules src/backend/* diff --git a/ts/package.json b/ts/package.json index 9f2cbd610..543952568 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,6 +1,10 @@ { "name": "anki", "version": "0.1.0", + "private": true, + "author": "Ankitects Pty Ltd and contributors", + "license": "AGPL-3.0-or-later", + "description": "Anki JS support files", "devDependencies": { "@pyoner/svelte-types": "^3.4.4-2", "@types/d3-array": "^2.0.0", @@ -10,9 +14,12 @@ "@types/d3-selection": "^1.4.1", "@types/d3-shape": "^1.3.2", "@types/d3-transition": "^1.1.6", + "@types/jquery": "^3.5.0", + "@types/jqueryui": "^1.12.13", "@types/lodash.debounce": "^4.0.6", "@types/lodash.throttle": "^4.1.6", "@types/long": "^4.0.1", + "@types/mathjax": "0.0.36", "@typescript-eslint/eslint-plugin": "^2.11.0", "@typescript-eslint/parser": "^2.11.0", "cross-env": "^7.0.2", @@ -23,6 +30,7 @@ "html-webpack-plugin": "^4.3.0", "prettier": "^2.0.0", "prettier-plugin-svelte": "^1.1.0", + "sass": "^1.26.9", "style-loader": "^1.2.1", "svelte": "^3.23.2", "svelte-loader": "^2.13.6", diff --git a/ts/src/stats/AddedGraph.svelte b/ts/src/stats/AddedGraph.svelte index a398ee470..84aefdeeb 100644 --- a/ts/src/stats/AddedGraph.svelte +++ b/ts/src/stats/AddedGraph.svelte @@ -4,16 +4,16 @@ import pb from "../backend/proto"; import HistogramGraph from "./HistogramGraph.svelte"; - export let data: pb.BackendProto.GraphsOut | null = null; + export let sourceData: pb.BackendProto.GraphsOut | null = null; let svg = null as HTMLElement | SVGElement | null; let histogramData = null as HistogramData | null; let range = AddedRange.Month; let addedData: GraphData | null = null; - $: if (data) { + $: if (sourceData) { console.log("gathering data"); - addedData = gatherData(data); + addedData = gatherData(sourceData); } $: if (addedData) { diff --git a/ts/src/stats/ButtonsGraph.svelte b/ts/src/stats/ButtonsGraph.svelte new file mode 100644 index 000000000..87704672c --- /dev/null +++ b/ts/src/stats/ButtonsGraph.svelte @@ -0,0 +1,32 @@ + + +
+

Answer Buttons

+ + + + + + + +
diff --git a/ts/src/stats/CardCounts.svelte b/ts/src/stats/CardCounts.svelte new file mode 100644 index 000000000..820e4d68b --- /dev/null +++ b/ts/src/stats/CardCounts.svelte @@ -0,0 +1,17 @@ + + +
+

Card Counts

+ {JSON.stringify(cardCounts)} +
diff --git a/ts/src/stats/EaseGraph.svelte b/ts/src/stats/EaseGraph.svelte index dc07bfa5f..1295db3b7 100644 --- a/ts/src/stats/EaseGraph.svelte +++ b/ts/src/stats/EaseGraph.svelte @@ -4,14 +4,14 @@ import pb from "../backend/proto"; import HistogramGraph from "./HistogramGraph.svelte"; - export let data: pb.BackendProto.GraphsOut | null = null; + export let sourceData: pb.BackendProto.GraphsOut | null = null; let svg = null as HTMLElement | SVGElement | null; let histogramData = null as HistogramData | null; - $: if (data) { + $: if (sourceData) { console.log("gathering data"); - histogramData = prepareData(gatherData(data)); + histogramData = prepareData(gatherData(sourceData)); } diff --git a/ts/src/stats/GraphsPage.svelte b/ts/src/stats/GraphsPage.svelte index 431f51bbf..20474fabd 100644 --- a/ts/src/stats/GraphsPage.svelte +++ b/ts/src/stats/GraphsPage.svelte @@ -11,8 +11,11 @@ import IntervalsGraph from "./IntervalsGraph.svelte"; import EaseGraph from "./EaseGraph.svelte"; import AddedGraph from "./AddedGraph.svelte"; + import TodayStats from "./TodayStats.svelte"; + import ButtonsGraph from "./ButtonsGraph.svelte"; + import CardCounts from "./CardCounts.svelte"; - let data: pb.BackendProto.GraphsOut | null = null; + let sourceData: pb.BackendProto.GraphsOut | null = null; enum SearchRange { Deck = 1, @@ -29,7 +32,7 @@ const refresh = async () => { console.log(`search is ${search}`); - data = await getGraphData(search, days); + sourceData = await getGraphData(search, days); }; $: { @@ -108,6 +111,9 @@ - - - + + + + + + diff --git a/ts/src/stats/IntervalsGraph.svelte b/ts/src/stats/IntervalsGraph.svelte index c0c69d571..079c48139 100644 --- a/ts/src/stats/IntervalsGraph.svelte +++ b/ts/src/stats/IntervalsGraph.svelte @@ -9,16 +9,16 @@ import pb from "../backend/proto"; import HistogramGraph from "./HistogramGraph.svelte"; - export let data: pb.BackendProto.GraphsOut | null = null; + export let sourceData: pb.BackendProto.GraphsOut | null = null; let svg = null as HTMLElement | SVGElement | null; let range = IntervalRange.Percentile95; let histogramData = null as HistogramData | null; let intervalData: IntervalGraphData | null = null; - $: if (data) { + $: if (sourceData) { console.log("gathering data"); - intervalData = gatherIntervalData(data); + intervalData = gatherIntervalData(sourceData); } $: if (intervalData) { diff --git a/ts/src/stats/TodayStats.svelte b/ts/src/stats/TodayStats.svelte new file mode 100644 index 000000000..40059c6ae --- /dev/null +++ b/ts/src/stats/TodayStats.svelte @@ -0,0 +1,17 @@ + + +
+

Today

+ {JSON.stringify(todayData)} +
diff --git a/ts/src/stats/buttons.ts b/ts/src/stats/buttons.ts new file mode 100644 index 000000000..8a4d9822d --- /dev/null +++ b/ts/src/stats/buttons.ts @@ -0,0 +1,195 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +/* eslint +@typescript-eslint/no-non-null-assertion: "off", +@typescript-eslint/no-explicit-any: "off", + */ + +import pb from "../backend/proto"; +import { interpolateRdYlGn } from "d3-scale-chromatic"; +import "d3-transition"; +import { select, mouse } from "d3-selection"; +import { scaleLinear, scaleBand, scaleSequential } from "d3-scale"; +import { axisBottom, axisLeft } from "d3-axis"; +import { showTooltip, hideTooltip } from "./tooltip"; +import { GraphBounds } from "./graphs"; + +type ButtonCounts = [number, number, number, number]; + +export interface GraphData { + learning: ButtonCounts; + young: ButtonCounts; + mature: ButtonCounts; +} + +const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind; + +export function gatherData(data: pb.BackendProto.GraphsOut): GraphData { + const learning: ButtonCounts = [0, 0, 0, 0]; + const young: ButtonCounts = [0, 0, 0, 0]; + const mature: ButtonCounts = [0, 0, 0, 0]; + + for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) { + let buttonNum = review.buttonChosen; + if (buttonNum <= 0 || buttonNum > 4) { + continue; + } + + let buttons = learning; + switch (review.reviewKind) { + case ReviewKind.LEARNING: + case ReviewKind.RELEARNING: + // V1 scheduler only had 3 buttons in learning + if (buttonNum === 4 && data.schedulerVersion === 1) { + buttonNum = 3; + } + break; + + case ReviewKind.REVIEW: + case ReviewKind.EARLY_REVIEW: + if (review.lastInterval < 21) { + buttons = young; + } else { + buttons = mature; + } + break; + } + + buttons[buttonNum - 1] += 1; + } + return { learning, young, mature }; +} + +interface Datum { + buttonNum: string; + group: "learning" | "young" | "mature"; + count: number; +} + +function tooltipText(d: Datum): string { + return JSON.stringify(d); +} + +export function renderButtons( + svgElem: SVGElement, + bounds: GraphBounds, + sourceData: GraphData +): void { + const data = [ + ...sourceData.learning.map((count: number, idx: number) => { + return { + buttonNum: (idx + 1).toString(), + group: "learning", + count, + } as Datum; + }), + ...sourceData.young.map((count: number, idx: number) => { + return { + buttonNum: (idx + 1).toString(), + group: "young", + count, + } as Datum; + }), + ...sourceData.mature.map((count: number, idx: number) => { + return { + buttonNum: (idx + 1).toString(), + group: "mature", + count, + } as Datum; + }), + ]; + + console.log(data); + + const yMax = Math.max(...data.map((d) => d.count)); + + const svg = select(svgElem); + const trans = svg.transition().duration(600) as any; + + const xGroup = scaleBand() + .domain(["learning", "young", "mature"]) + .range([bounds.marginLeft, bounds.width - bounds.marginRight]); + svg.select(".x-ticks").transition(trans).call( + axisBottom(xGroup) + // .ticks() + .tickSizeOuter(0) + ); + + const xButton = scaleBand() + .domain(["1", "2", "3", "4"]) + .range([0, xGroup.bandwidth()]) + .paddingOuter(1) + .paddingInner(0.1); + + const colour = scaleSequential(interpolateRdYlGn).domain([1, 4]); + + // y scale + + const y = scaleLinear() + .range([bounds.height - bounds.marginBottom, bounds.marginTop]) + .domain([0, yMax]); + svg.select(".y-ticks") + .transition(trans) + .call( + axisLeft(y) + .ticks(bounds.height / 80) + .tickSizeOuter(0) + ); + + // x bars + + const updateBar = (sel: any): any => { + return sel + .attr("width", xButton.bandwidth()) + .attr("opacity", (d: Datum) => { + switch (d.group) { + case "learning": + return 0.3; + case "young": + return 0.5; + case "mature": + return 1; + } + }) + .transition(trans) + .attr("x", (d: Datum) => xGroup(d.group)! + xButton(d.buttonNum)!) + .attr("y", (d: Datum) => y(d.count)!) + .attr("height", (d: Datum) => y(0) - y(d.count)) + .attr("fill", (d: Datum) => colour(parseInt(d.buttonNum))); + }; + + svg.select("g.bars") + .selectAll("rect") + .data(data) + .join( + (enter) => + enter + .append("rect") + .attr("rx", 1) + .attr("x", (d: Datum) => xGroup(d.group)! + xButton(d.buttonNum)!) + .attr("y", y(0)) + .attr("height", 0) + .call(updateBar), + (update) => update.call(updateBar), + (remove) => + remove.call((remove) => + remove.transition(trans).attr("height", 0).attr("y", y(0)) + ) + ); + + // hover/tooltip + svg.select("g.hoverzone") + .selectAll("rect") + .data(data) + .join("rect") + .attr("x", (d: Datum) => xGroup(d.group)! + xButton(d.buttonNum)!) + .attr("y", () => y(yMax!)) + .attr("width", xButton.bandwidth()) + .attr("height", () => y(0) - y(yMax!)) + .on("mousemove", function (this: any, d: Datum) { + const [x, y] = mouse(document.body); + showTooltip(tooltipText(d), x, y); + }) + .on("mouseout", hideTooltip); +} diff --git a/ts/src/stats/card-counts.ts b/ts/src/stats/card-counts.ts new file mode 100644 index 000000000..35bd5a081 --- /dev/null +++ b/ts/src/stats/card-counts.ts @@ -0,0 +1,60 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import pb from "../backend/proto"; +import { CardQueue } from "../cards"; + +export interface CardCounts { + totalCards: number; + totalNotes: number; + newCards: number; + young: number; + mature: number; + suspended: number; + buried: number; +} + +export function gatherData(data: pb.BackendProto.GraphsOut): CardCounts { + const totalNotes = data.noteCount; + const totalCards = data.cards.length; + let newCards = 0; + let young = 0; + let mature = 0; + let suspended = 0; + let buried = 0; + + for (const card of data.cards as pb.BackendProto.Card[]) { + switch (card.queue) { + case CardQueue.New: + newCards += 1; + break; + case CardQueue.Review: + if (card.ivl >= 21) { + mature += 1; + break; + } + // young falls through + case CardQueue.Learn: + case CardQueue.DayLearn: + young += 1; + break; + case CardQueue.Suspended: + suspended += 1; + break; + case CardQueue.SchedBuried: + case CardQueue.UserBuried: + buried += 1; + break; + } + } + + return { + totalCards, + totalNotes, + newCards, + young, + mature, + suspended, + buried, + }; +} diff --git a/ts/src/stats/hours.ts b/ts/src/stats/hours.ts new file mode 100644 index 000000000..b6ed33163 --- /dev/null +++ b/ts/src/stats/hours.ts @@ -0,0 +1,41 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import pb from "../backend/proto"; + +type ButtonCounts = [number, number, number, number]; + +interface Hour { + hour: number; + totalCount: number; + correctCount: number; +} + +export interface GraphData { + hours: Hour[]; +} + +const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind; + +export function gatherData(data: pb.BackendProto.GraphsOut): GraphData { + const hours = Array(24).map((n: number) => { + return { hour: n, totalCount: 0, correctCount: 0 } as Hour; + }); + + // fixme: relative to midnight, not rollover + + for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) { + if (review.reviewKind == ReviewKind.EARLY_REVIEW) { + continue; + } + + const hour = + (((review.id as number) / 1000 + data.localOffsetSecs) / 3600) % 24; + hours[hour].totalCount += 1; + if (review.buttonChosen != 1) { + hours[hour].correctCount += 1; + } + } + + return { hours }; +} diff --git a/ts/src/stats/today.ts b/ts/src/stats/today.ts new file mode 100644 index 000000000..6fce03c1f --- /dev/null +++ b/ts/src/stats/today.ts @@ -0,0 +1,83 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import pb from "../backend/proto"; + +export interface TodayData { + answerCount: number; + answerMillis: number; + correctCount: number; + matureCorrect: number; + matureCount: number; + learnCount: number; + reviewCount: number; + relearnCount: number; + earlyReviewCount: number; +} + +const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind; + +export function gatherData(data: pb.BackendProto.GraphsOut): TodayData { + let answerCount = 0; + let answerMillis = 0; + let correctCount = 0; + let matureCorrect = 0; + let matureCount = 0; + let learnCount = 0; + let reviewCount = 0; + let relearnCount = 0; + let earlyReviewCount = 0; + + const startOfTodayMillis = (data.nextDayAtSecs - 86400) * 1000; + + for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) { + if (review.id < startOfTodayMillis) { + continue; + } + + // total + answerCount += 1; + answerMillis += review.takenMillis; + + // correct + if (review.buttonChosen > 1) { + correctCount += 1; + } + + // mature + if (review.lastInterval >= 21) { + matureCount += 1; + if (review.buttonChosen > 1) { + matureCorrect += 1; + } + } + + // type counts + switch (review.reviewKind) { + case ReviewKind.LEARNING: + learnCount += 1; + break; + case ReviewKind.REVIEW: + reviewCount += 1; + break; + case ReviewKind.RELEARNING: + relearnCount += 1; + break; + case ReviewKind.EARLY_REVIEW: + earlyReviewCount += 1; + break; + } + } + + return { + answerCount, + answerMillis, + correctCount, + matureCorrect, + matureCount, + learnCount, + reviewCount, + relearnCount, + earlyReviewCount, + }; +}