mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
add top level component and pass search/day limit back from frontend
This commit is contained in:
parent
6fd444b958
commit
dcff5e28fa
8 changed files with 119 additions and 40 deletions
|
@ -14,6 +14,7 @@ from typing import Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
|
from anki.rsbackend import from_json_bytes
|
||||||
from anki.utils import devMode
|
from anki.utils import devMode
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import aqt_data_folder
|
from aqt.utils import aqt_data_folder
|
||||||
|
@ -78,7 +79,7 @@ class MediaServer(threading.Thread):
|
||||||
|
|
||||||
class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
timeout = 1
|
timeout = 10
|
||||||
mw: Optional[aqt.main.AnkiQt] = None
|
mw: Optional[aqt.main.AnkiQt] = None
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
|
@ -181,7 +182,9 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
cmd = self.path[len("/_anki/") :]
|
cmd = self.path[len("/_anki/") :]
|
||||||
|
|
||||||
if cmd == "graphData":
|
if cmd == "graphData":
|
||||||
data = graph_data(self.mw.col)
|
content_length = int(self.headers['Content-Length'])
|
||||||
|
body = self.rfile.read(content_length)
|
||||||
|
data = graph_data(self.mw.col, **from_json_bytes(body))
|
||||||
else:
|
else:
|
||||||
self.send_error(HTTPStatus.NOT_FOUND, "Method not found")
|
self.send_error(HTTPStatus.NOT_FOUND, "Method not found")
|
||||||
return
|
return
|
||||||
|
@ -195,10 +198,13 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
self.wfile.write(data)
|
self.wfile.write(data)
|
||||||
|
|
||||||
|
|
||||||
def graph_data(col: Collection) -> bytes:
|
def graph_data(col: Collection, search: str, days: str) -> bytes:
|
||||||
graphs = col.backend.graphs(search="", days=0)
|
try:
|
||||||
return graphs
|
return col.backend.graphs(search=search, days=days)
|
||||||
|
except Exception as e:
|
||||||
|
# likely searching error
|
||||||
|
print(e)
|
||||||
|
return b''
|
||||||
|
|
||||||
# work around Windows machines with incorrect mime type
|
# work around Windows machines with incorrect mime type
|
||||||
RequestHandler.extensions_map[".css"] = "text/css"
|
RequestHandler.extensions_map[".css"] = "text/css"
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"@types/d3-selection": "^1.4.1",
|
"@types/d3-selection": "^1.4.1",
|
||||||
"@types/d3-shape": "^1.3.2",
|
"@types/d3-shape": "^1.3.2",
|
||||||
"@types/d3-transition": "^1.1.6",
|
"@types/d3-transition": "^1.1.6",
|
||||||
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/lodash.throttle": "^4.1.6",
|
"@types/lodash.throttle": "^4.1.6",
|
||||||
"@types/long": "^4.0.1",
|
"@types/long": "^4.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.11.0",
|
"@typescript-eslint/eslint-plugin": "^2.11.0",
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
"d3-selection": "^1.4.1",
|
"d3-selection": "^1.4.1",
|
||||||
"d3-shape": "^1.3.7",
|
"d3-shape": "^1.3.7",
|
||||||
"d3-transition": "^1.3.2",
|
"d3-transition": "^1.3.2",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"protobufjs": "^6.9.0"
|
"protobufjs": "^6.9.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,9 +4,11 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="svelte"></div>
|
<div id="main"></div>
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
anki.renderGraphs();
|
new anki.default({
|
||||||
|
target: document.getElementById("main"),
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
65
ts/src/stats/GraphsPage.svelte
Normal file
65
ts/src/stats/GraphsPage.svelte
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<script context="module">
|
||||||
|
import style from "./graphs.css";
|
||||||
|
|
||||||
|
document.head.append(style);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="typescript">
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
|
||||||
|
import { assertUnreachable } from "../typing";
|
||||||
|
import pb from "../backend/proto";
|
||||||
|
import { getGraphData, GraphRange } from "./graphs";
|
||||||
|
import IntervalsGraph from "./IntervalsGraph.svelte";
|
||||||
|
|
||||||
|
let data: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
|
||||||
|
let search = "deck:current";
|
||||||
|
let range: GraphRange = GraphRange.Month;
|
||||||
|
let days: number = 31;
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
data = await getGraphData(search, days);
|
||||||
|
};
|
||||||
|
|
||||||
|
$: {
|
||||||
|
switch (range) {
|
||||||
|
case GraphRange.Month:
|
||||||
|
days = 31;
|
||||||
|
break;
|
||||||
|
case GraphRange.Year:
|
||||||
|
days = 365;
|
||||||
|
break;
|
||||||
|
case GraphRange.All:
|
||||||
|
days = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assertUnreachable(range);
|
||||||
|
}
|
||||||
|
console.log("refresh");
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleRefresh = debounce(() => {
|
||||||
|
refresh();
|
||||||
|
}, 1000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="range-box">
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={GraphRange.Month} />
|
||||||
|
Month
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={GraphRange.Year} />
|
||||||
|
Year
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={GraphRange.All} />
|
||||||
|
All
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="text" bind:value={search} on:input={scheduleRefresh} />
|
||||||
|
|
||||||
|
<IntervalsGraph {data} />
|
|
@ -9,7 +9,7 @@
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
|
|
||||||
export let cards: pb.BackendProto.Card[] | null = null;
|
export let data: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
|
||||||
let svg = null as HTMLElement | SVGElement | null;
|
let svg = null as HTMLElement | SVGElement | null;
|
||||||
let updater = null as IntervalUpdateFn | null;
|
let updater = null as IntervalUpdateFn | null;
|
||||||
|
@ -20,10 +20,14 @@
|
||||||
|
|
||||||
let range = IntervalRange.Percentile95;
|
let range = IntervalRange.Percentile95;
|
||||||
|
|
||||||
let graphData: IntervalGraphData;
|
let intervalData: IntervalGraphData | null = null;
|
||||||
$: graphData = gatherIntervalData(cards!);
|
$: if (data) {
|
||||||
$: if (updater) {
|
console.log("gathering data");
|
||||||
updater(graphData, range);
|
intervalData = gatherIntervalData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (intervalData && updater) {
|
||||||
|
updater(intervalData, range);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -7,40 +7,35 @@
|
||||||
@typescript-eslint/ban-ts-ignore: "off" */
|
@typescript-eslint/ban-ts-ignore: "off" */
|
||||||
|
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import style from "./graphs.css";
|
|
||||||
|
|
||||||
async function fetchData(): Promise<Uint8Array> {
|
async function fetchData(search: string, days: number): Promise<Uint8Array> {
|
||||||
let t = performance.now();
|
|
||||||
const resp = await fetch("/_anki/graphData", {
|
const resp = await fetch("/_anki/graphData", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
search,
|
||||||
|
days,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw Error(`unexpected reply: ${resp.statusText}`);
|
throw Error(`unexpected reply: ${resp.statusText}`);
|
||||||
}
|
}
|
||||||
console.log(`fetch in ${performance.now() - t}ms`);
|
|
||||||
t = performance.now();
|
|
||||||
// get returned bytes
|
// get returned bytes
|
||||||
const respBlob = await resp.blob();
|
const respBlob = await resp.blob();
|
||||||
const respBuf = await new Response(respBlob).arrayBuffer();
|
const respBuf = await new Response(respBlob).arrayBuffer();
|
||||||
const bytes = new Uint8Array(respBuf);
|
const bytes = new Uint8Array(respBuf);
|
||||||
console.log(`bytes in ${performance.now() - t}ms`);
|
|
||||||
t = performance.now();
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
import IntervalGraph from "./IntervalsGraph.svelte";
|
export async function getGraphData(
|
||||||
|
search: string,
|
||||||
export async function renderGraphs(): Promise<void> {
|
days: number
|
||||||
const bytes = await fetchData();
|
): Promise<pb.BackendProto.GraphsOut> {
|
||||||
let t = performance.now();
|
const bytes = await fetchData(search, days);
|
||||||
const data = pb.BackendProto.GraphsOut.decode(bytes);
|
return pb.BackendProto.GraphsOut.decode(bytes);
|
||||||
console.log(`decode in ${performance.now() - t}ms`);
|
}
|
||||||
t = performance.now();
|
|
||||||
|
export enum GraphRange {
|
||||||
document.head.append(style);
|
Month = 1,
|
||||||
|
Year = 2,
|
||||||
new IntervalGraph({
|
All = 3,
|
||||||
target: document.getElementById("svelte")!,
|
|
||||||
props: { cards: data.cards2 },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ export enum IntervalRange {
|
||||||
All = 4,
|
All = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gatherIntervalData(cards: pb.BackendProto.Card[]): IntervalGraphData {
|
export function gatherIntervalData(data: pb.BackendProto.GraphsOut): IntervalGraphData {
|
||||||
const intervals = cards
|
const intervals = (data.cards2 as pb.BackendProto.Card[])
|
||||||
.filter((c) => c.queue == CardQueue.Review)
|
.filter((c) => c.queue == CardQueue.Review)
|
||||||
.map((c) => c.ivl);
|
.map((c) => c.ivl);
|
||||||
return { intervals };
|
return { intervals };
|
||||||
|
@ -152,11 +152,16 @@ export function intervalGraph(svgElem: SVGElement): IntervalUpdateFn {
|
||||||
|
|
||||||
yAxisGroup.transition(t as any).call(updateYAxis as any, y);
|
yAxisGroup.transition(t as any).call(updateYAxis as any, y);
|
||||||
|
|
||||||
|
function barWidth(d: any): number {
|
||||||
|
const width = Math.max(0, x(d.x1) - x(d.x0) - 1);
|
||||||
|
return width ? width : 0;
|
||||||
|
}
|
||||||
|
|
||||||
const updateBar = (sel: any) => {
|
const updateBar = (sel: any) => {
|
||||||
return sel.call((sel) =>
|
return sel.call((sel) =>
|
||||||
sel
|
sel
|
||||||
.transition(t as any)
|
.transition(t as any)
|
||||||
.attr("width", (d: any) => Math.max(0, x(d.x1) - x(d.x0) - 1))
|
.attr("width", barWidth)
|
||||||
.attr("x", (d: any) => x(d.x0))
|
.attr("x", (d: any) => x(d.x0))
|
||||||
.attr("y", (d: any) => y(d.length)!)
|
.attr("y", (d: any) => y(d.length)!)
|
||||||
.attr("height", (d: any) => y(0) - y(d.length))
|
.attr("height", (d: any) => y(0) - y(d.length))
|
||||||
|
@ -210,7 +215,7 @@ export function intervalGraph(svgElem: SVGElement): IntervalUpdateFn {
|
||||||
.join("rect")
|
.join("rect")
|
||||||
.attr("x", (d: any) => x(d.x0))
|
.attr("x", (d: any) => x(d.x0))
|
||||||
.attr("y", () => y(yMax!))
|
.attr("y", () => y(yMax!))
|
||||||
.attr("width", (d: any) => Math.max(0, x(d.x1) - x(d.x0) - 1))
|
.attr("width", barWidth)
|
||||||
.attr("height", () => y(0) - y(yMax!))
|
.attr("height", () => y(0) - y(yMax!))
|
||||||
.attr("fill", "none")
|
.attr("fill", "none")
|
||||||
.attr("pointer-events", "all")
|
.attr("pointer-events", "all")
|
||||||
|
|
|
@ -5,7 +5,7 @@ var path = require("path");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
graphs: ["./src/stats/graphs.ts"],
|
graphs: ["./src/stats/GraphsPage.svelte"],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
library: "anki",
|
library: "anki",
|
||||||
|
|
Loading…
Reference in a new issue