add top level component and pass search/day limit back from frontend

This commit is contained in:
Damien Elmes 2020-06-22 19:11:50 +10:00
parent 6fd444b958
commit dcff5e28fa
8 changed files with 119 additions and 40 deletions

View file

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

View file

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

View file

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

View 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} />

View file

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

View file

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

View file

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

View file

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