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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ var path = require("path");
module.exports = {
entry: {
graphs: ["./src/stats/graphs.ts"],
graphs: ["./src/stats/GraphsPage.svelte"],
},
output: {
library: "anki",