qt's js code now shares ts/node_modules; more graph work

This commit is contained in:
Damien Elmes 2020-06-26 10:42:10 +10:00
parent 2a3ef80c90
commit 329186f140
22 changed files with 556 additions and 349 deletions

View file

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

View file

@ -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;
}

View file

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

View file

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

34
qt/ts/Makefile Normal file
View file

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

185
qt/ts/package-lock.json generated
View file

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

View file

@ -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"
}
}

View file

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

View file

@ -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<RevlogEntry> 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,
}
}
}

View file

@ -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/*

View file

@ -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",

View file

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

View file

@ -0,0 +1,32 @@
<script lang="typescript">
import { defaultGraphBounds } from "./graphs";
import AxisTicks from "./AxisTicks.svelte";
import AxisLabels from "./AxisLabels.svelte";
import { gatherData, GraphData, renderButtons } from "./buttons";
import pb from "../backend/proto";
import HistogramGraph from "./HistogramGraph.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null;
const bounds = defaultGraphBounds();
const xText = "";
const yText = "Times pressed";
let svg = null as HTMLElement | SVGElement | null;
$: if (sourceData) {
console.log("gathering data");
renderButtons(svg as SVGElement, bounds, gatherData(sourceData));
}
</script>
<div class="graph">
<h1>Answer Buttons</h1>
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
<g class="bars" />
<g class="hoverzone" />
<AxisTicks {bounds} />
<AxisLabels {bounds} {xText} {yText} />
</svg>
</div>

View file

@ -0,0 +1,17 @@
<script lang="typescript">
import { gatherData, CardCounts } from "./card-counts";
import pb from "../backend/proto";
export let sourceData: pb.BackendProto.GraphsOut | null = null;
let cardCounts: CardCounts | null = null;
$: if (sourceData) {
console.log("gathering data");
cardCounts = gatherData(sourceData);
}
</script>
<div class="graph">
<h1>Card Counts</h1>
{JSON.stringify(cardCounts)}
</div>

View file

@ -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));
}
</script>

View file

@ -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 @@
</label>
</div>
<AddedGraph {data} />
<IntervalsGraph {data} />
<EaseGraph {data} />
<TodayStats {sourceData} />
<CardCounts {sourceData} />
<AddedGraph {sourceData} />
<IntervalsGraph {sourceData} />
<EaseGraph {sourceData} />
<ButtonsGraph {sourceData} />

View file

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

View file

@ -0,0 +1,17 @@
<script lang="typescript">
import { gatherData, TodayData } from "./today";
import pb from "../backend/proto";
export let sourceData: pb.BackendProto.GraphsOut | null = null;
let todayData: TodayData | null = null;
$: if (sourceData) {
console.log("gathering data");
todayData = gatherData(sourceData);
}
</script>
<div class="graph">
<h1>Today</h1>
{JSON.stringify(todayData)}
</div>

195
ts/src/stats/buttons.ts Normal file
View file

@ -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<SVGGElement>(".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<SVGGElement>(".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);
}

View file

@ -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,
};
}

41
ts/src/stats/hours.ts Normal file
View file

@ -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 };
}

83
ts/src/stats/today.ts Normal file
View file

@ -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,
};
}