mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
use graph for card counts
This commit is contained in:
parent
5574c0dfb3
commit
d305a3a2cc
4 changed files with 125 additions and 28 deletions
2
ts/d3_missing.d.ts
vendored
2
ts/d3_missing.d.ts
vendored
|
@ -1,4 +1,4 @@
|
||||||
import "d3-array";
|
import "d3-array";
|
||||||
declare module "d3-array" {
|
declare module "d3-array" {
|
||||||
export function cumsum(arg0: number[]): Float64Array;
|
export function cumsum(arg0: any[], arg1?: (any) => number): Float64Array;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,41 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { gatherData, CardCounts } from "./card-counts";
|
import { defaultGraphBounds } from "./graphs";
|
||||||
|
import { gatherData, GraphData, renderCards } from "./card-counts";
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
|
||||||
let cardCounts: CardCounts | null = null;
|
let svg = null as HTMLElement | SVGElement | null;
|
||||||
|
|
||||||
|
let bounds = defaultGraphBounds();
|
||||||
|
bounds.height = 20;
|
||||||
|
bounds.marginLeft = 20;
|
||||||
|
bounds.marginRight = 20;
|
||||||
|
bounds.marginTop = 0;
|
||||||
|
|
||||||
|
let graphData: GraphData | null = null;
|
||||||
$: if (sourceData) {
|
$: if (sourceData) {
|
||||||
cardCounts = gatherData(sourceData, i18n);
|
graphData = gatherData(sourceData, i18n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (graphData) {
|
||||||
|
renderCards(svg as any, bounds, graphData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = i18n.tr(i18n.TR.STATISTICS_COUNTS_TOTAL_CARDS);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
{#if graphData}
|
||||||
.counts-outer {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
{#if cardCounts}
|
|
||||||
<div class="graph">
|
<div class="graph">
|
||||||
<h1>{cardCounts.title}</h1>
|
<h1>{graphData.title}</h1>
|
||||||
<div class="counts-outer">
|
|
||||||
{#each cardCounts.counts as count}
|
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||||
<div>
|
<g class="days" />
|
||||||
<div>
|
</svg>
|
||||||
<b>{count[0]}</b>
|
|
||||||
</div>
|
<div class="centered">{total}: {graphData.totalCards}</div>
|
||||||
<div>{count[1]}</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,17 +1,31 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import pb from "../backend/proto";
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
@typescript-eslint/no-explicit-any: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
import { CardQueue } from "../cards";
|
import { CardQueue } from "../cards";
|
||||||
|
import pb from "../backend/proto";
|
||||||
|
import { schemeGreens, schemeBlues } from "d3-scale-chromatic";
|
||||||
|
import "d3-transition";
|
||||||
|
import { select, mouse } from "d3-selection";
|
||||||
|
import { scaleLinear } from "d3-scale";
|
||||||
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
|
import { GraphBounds } from "./graphs";
|
||||||
|
import { cumsum } from "d3-array";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
|
||||||
type Count = [string, number];
|
type Count = [string, number];
|
||||||
export interface CardCounts {
|
export interface GraphData {
|
||||||
title: string;
|
title: string;
|
||||||
counts: Count[];
|
counts: Count[];
|
||||||
|
totalCards: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): CardCounts {
|
export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): GraphData {
|
||||||
|
// fixme: handle preview cards
|
||||||
const totalCards = data.cards.length;
|
const totalCards = data.cards.length;
|
||||||
let newCards = 0;
|
let newCards = 0;
|
||||||
let young = 0;
|
let young = 0;
|
||||||
|
@ -45,7 +59,6 @@ export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): CardCou
|
||||||
}
|
}
|
||||||
|
|
||||||
const counts = [
|
const counts = [
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_TOTAL_CARDS), totalCards] as Count,
|
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards] as Count,
|
[i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards] as Count,
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS), young] as Count,
|
[i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS), young] as Count,
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_MATURE_CARDS), mature] as Count,
|
[i18n.tr(i18n.TR.STATISTICS_COUNTS_MATURE_CARDS), mature] as Count,
|
||||||
|
@ -56,5 +69,81 @@ export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): CardCou
|
||||||
return {
|
return {
|
||||||
title: i18n.tr(i18n.TR.STATISTICS_COUNTS_TITLE),
|
title: i18n.tr(i18n.TR.STATISTICS_COUNTS_TITLE),
|
||||||
counts,
|
counts,
|
||||||
|
totalCards,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Reviews {
|
||||||
|
mature: number;
|
||||||
|
young: number;
|
||||||
|
learn: number;
|
||||||
|
relearn: number;
|
||||||
|
early: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderCards(
|
||||||
|
svgElem: SVGElement,
|
||||||
|
bounds: GraphBounds,
|
||||||
|
sourceData: GraphData
|
||||||
|
): void {
|
||||||
|
const summed = cumsum(sourceData.counts, (d) => d[1]);
|
||||||
|
const data = Array.from(summed).map((n, idx) => {
|
||||||
|
return {
|
||||||
|
count: sourceData.counts[idx],
|
||||||
|
idx,
|
||||||
|
total: n,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const xMax = summed.slice(-1)[0];
|
||||||
|
const x = scaleLinear().domain([0, xMax]);
|
||||||
|
const svg = select(svgElem);
|
||||||
|
const trans = svg.transition().duration(600) as any;
|
||||||
|
|
||||||
|
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||||
|
|
||||||
|
const tooltipText = (d: any): string => {
|
||||||
|
const pct = ((d.count[1] / xMax) * 100).toFixed(2);
|
||||||
|
return `${d.count[0]}: ${d.count[1]} (${pct}%)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBar = (sel: any): any => {
|
||||||
|
return sel
|
||||||
|
.on("mousemove", function (this: any, d: any) {
|
||||||
|
const [x, y] = mouse(document.body);
|
||||||
|
showTooltip(tooltipText(d), x, y);
|
||||||
|
})
|
||||||
|
.transition(trans)
|
||||||
|
.attr("width", (d) => x(d.total) - bounds.marginLeft);
|
||||||
|
};
|
||||||
|
|
||||||
|
data.reverse();
|
||||||
|
svg.select("g.days")
|
||||||
|
.selectAll("rect")
|
||||||
|
.data(data)
|
||||||
|
.join(
|
||||||
|
(enter) =>
|
||||||
|
enter
|
||||||
|
.append("rect")
|
||||||
|
.attr("height", 10)
|
||||||
|
.attr("x", x(0))
|
||||||
|
.attr("y", bounds.marginTop)
|
||||||
|
.attr("fill", (d: any): any => {
|
||||||
|
switch (d.idx) {
|
||||||
|
case 0:
|
||||||
|
return schemeBlues[5][2];
|
||||||
|
case 1:
|
||||||
|
return schemeGreens[5][2];
|
||||||
|
case 2:
|
||||||
|
return schemeGreens[5][3];
|
||||||
|
case 3:
|
||||||
|
return "#FFDC41";
|
||||||
|
case 4:
|
||||||
|
return "grey";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("mouseout", hideTooltip)
|
||||||
|
|
||||||
|
.call((d) => updateBar(d)),
|
||||||
|
(update) => update.call((d) => updateBar(d))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -139,3 +139,7 @@ body.night-mode {
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue