mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02: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
|
||||
from anki.collection import Collection
|
||||
from anki.rsbackend import from_json_bytes
|
||||
from anki.utils import devMode
|
||||
from aqt.qt import *
|
||||
from aqt.utils import aqt_data_folder
|
||||
|
@ -78,7 +79,7 @@ class MediaServer(threading.Thread):
|
|||
|
||||
class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
|
||||
timeout = 1
|
||||
timeout = 10
|
||||
mw: Optional[aqt.main.AnkiQt] = None
|
||||
|
||||
def do_GET(self):
|
||||
|
@ -181,7 +182,9 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
cmd = self.path[len("/_anki/") :]
|
||||
|
||||
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:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "Method not found")
|
||||
return
|
||||
|
@ -195,10 +198,13 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
self.wfile.write(data)
|
||||
|
||||
|
||||
def graph_data(col: Collection) -> bytes:
|
||||
graphs = col.backend.graphs(search="", days=0)
|
||||
return graphs
|
||||
|
||||
def graph_data(col: Collection, search: str, days: str) -> bytes:
|
||||
try:
|
||||
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
|
||||
RequestHandler.extensions_map[".css"] = "text/css"
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"@types/d3-selection": "^1.4.1",
|
||||
"@types/d3-shape": "^1.3.2",
|
||||
"@types/d3-transition": "^1.1.6",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.throttle": "^4.1.6",
|
||||
"@types/long": "^4.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^2.11.0",
|
||||
|
@ -47,6 +48,7 @@
|
|||
"d3-selection": "^1.4.1",
|
||||
"d3-shape": "^1.3.7",
|
||||
"d3-transition": "^1.3.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"protobufjs": "^6.9.0"
|
||||
},
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
<meta charset="utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="svelte"></div>
|
||||
<div id="main"></div>
|
||||
</body>
|
||||
<script>
|
||||
anki.renderGraphs();
|
||||
new anki.default({
|
||||
target: document.getElementById("main"),
|
||||
});
|
||||
</script>
|
||||
</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 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 updater = null as IntervalUpdateFn | null;
|
||||
|
@ -20,10 +20,14 @@
|
|||
|
||||
let range = IntervalRange.Percentile95;
|
||||
|
||||
let graphData: IntervalGraphData;
|
||||
$: graphData = gatherIntervalData(cards!);
|
||||
$: if (updater) {
|
||||
updater(graphData, range);
|
||||
let intervalData: IntervalGraphData | null = null;
|
||||
$: if (data) {
|
||||
console.log("gathering data");
|
||||
intervalData = gatherIntervalData(data);
|
||||
}
|
||||
|
||||
$: if (intervalData && updater) {
|
||||
updater(intervalData, range);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -7,40 +7,35 @@
|
|||
@typescript-eslint/ban-ts-ignore: "off" */
|
||||
|
||||
import pb from "../backend/proto";
|
||||
import style from "./graphs.css";
|
||||
|
||||
async function fetchData(): Promise<Uint8Array> {
|
||||
let t = performance.now();
|
||||
async function fetchData(search: string, days: number): Promise<Uint8Array> {
|
||||
const resp = await fetch("/_anki/graphData", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
search,
|
||||
days,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw Error(`unexpected reply: ${resp.statusText}`);
|
||||
}
|
||||
console.log(`fetch in ${performance.now() - t}ms`);
|
||||
t = performance.now();
|
||||
// get returned bytes
|
||||
const respBlob = await resp.blob();
|
||||
const respBuf = await new Response(respBlob).arrayBuffer();
|
||||
const bytes = new Uint8Array(respBuf);
|
||||
console.log(`bytes in ${performance.now() - t}ms`);
|
||||
t = performance.now();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
import IntervalGraph from "./IntervalsGraph.svelte";
|
||||
|
||||
export async function renderGraphs(): Promise<void> {
|
||||
const bytes = await fetchData();
|
||||
let t = performance.now();
|
||||
const data = pb.BackendProto.GraphsOut.decode(bytes);
|
||||
console.log(`decode in ${performance.now() - t}ms`);
|
||||
t = performance.now();
|
||||
|
||||
document.head.append(style);
|
||||
|
||||
new IntervalGraph({
|
||||
target: document.getElementById("svelte")!,
|
||||
props: { cards: data.cards2 },
|
||||
});
|
||||
export async function getGraphData(
|
||||
search: string,
|
||||
days: number
|
||||
): Promise<pb.BackendProto.GraphsOut> {
|
||||
const bytes = await fetchData(search, days);
|
||||
return pb.BackendProto.GraphsOut.decode(bytes);
|
||||
}
|
||||
|
||||
export enum GraphRange {
|
||||
Month = 1,
|
||||
Year = 2,
|
||||
All = 3,
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@ export enum IntervalRange {
|
|||
All = 4,
|
||||
}
|
||||
|
||||
export function gatherIntervalData(cards: pb.BackendProto.Card[]): IntervalGraphData {
|
||||
const intervals = cards
|
||||
export function gatherIntervalData(data: pb.BackendProto.GraphsOut): IntervalGraphData {
|
||||
const intervals = (data.cards2 as pb.BackendProto.Card[])
|
||||
.filter((c) => c.queue == CardQueue.Review)
|
||||
.map((c) => c.ivl);
|
||||
return { intervals };
|
||||
|
@ -152,11 +152,16 @@ export function intervalGraph(svgElem: SVGElement): IntervalUpdateFn {
|
|||
|
||||
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) => {
|
||||
return sel.call((sel) =>
|
||||
sel
|
||||
.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("y", (d: any) => y(d.length)!)
|
||||
.attr("height", (d: any) => y(0) - y(d.length))
|
||||
|
@ -210,7 +215,7 @@ export function intervalGraph(svgElem: SVGElement): IntervalUpdateFn {
|
|||
.join("rect")
|
||||
.attr("x", (d: any) => x(d.x0))
|
||||
.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("fill", "none")
|
||||
.attr("pointer-events", "all")
|
||||
|
|
|
@ -5,7 +5,7 @@ var path = require("path");
|
|||
|
||||
module.exports = {
|
||||
entry: {
|
||||
graphs: ["./src/stats/graphs.ts"],
|
||||
graphs: ["./src/stats/GraphsPage.svelte"],
|
||||
},
|
||||
output: {
|
||||
library: "anki",
|
||||
|
|
Loading…
Reference in a new issue