Merge pull request #892 from hgiesel/cardcounts

Add "By card type" and "By scheduling queue" variants of Card Counts stats graph
This commit is contained in:
Damien Elmes 2021-01-06 19:11:47 +10:00 committed by GitHub
commit 844b725809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 114 additions and 57 deletions

View File

@ -87,6 +87,7 @@ statistics-counts-early-cards = Early
statistics-counts-learning-cards = Learning statistics-counts-learning-cards = Learning
statistics-counts-relearning-cards = Relearning statistics-counts-relearning-cards = Relearning
statistics-counts-title = Card Counts statistics-counts-title = Card Counts
statistics-counts-separate-suspended-buried-cards = Separate suspended / buried cards
statistics-range-all-time = all statistics-range-all-time = all
statistics-range-1-year-history = last 12 months statistics-range-1-year-history = last 12 months
statistics-range-all-history = all history statistics-range-all-history = all history

View File

@ -4,10 +4,12 @@
import type { GraphData, TableDatum } from "./card-counts"; import type { GraphData, TableDatum } from "./card-counts";
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import type { I18n } from "anki/i18n"; import type { I18n } from "anki/i18n";
import SeparateInactiveCheckbox from "./SeparateInactiveCheckbox.svelte";
export let sourceData: pb.BackendProto.GraphsOut; export let sourceData: pb.BackendProto.GraphsOut;
export let i18n: I18n; export let i18n: I18n;
let separateInactive = false;
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
let bounds = defaultGraphBounds(); let bounds = defaultGraphBounds();
@ -16,8 +18,9 @@
let graphData = (null as unknown) as GraphData; let graphData = (null as unknown) as GraphData;
let tableData = (null as unknown) as TableDatum[]; let tableData = (null as unknown) as TableDatum[];
$: { $: {
graphData = gatherData(sourceData, i18n); graphData = gatherData(sourceData, separateInactive, i18n);
tableData = renderCards(svg as any, bounds, graphData); tableData = renderCards(svg as any, bounds, graphData);
} }
@ -52,6 +55,10 @@
<div class="graph" id="graph-card-counts"> <div class="graph" id="graph-card-counts">
<h1>{graphData.title}</h1> <h1>{graphData.title}</h1>
<div class="range-box-inner">
<SeparateInactiveCheckbox bind:separateInactive {i18n} />
</div>
<div class="counts-outer"> <div class="counts-outer">
<svg <svg
bind:this={svg} bind:this={svg}

View File

@ -0,0 +1,10 @@
<script lang="typescript">
import type { I18n } from "anki/i18n";
export let i18n: I18n;
export let separateInactive: boolean = false;
const label = i18n.tr(i18n.TR.STATISTICS_COUNTS_SEPARATE_SUSPENDED_BURIED_CARDS);
</script>
<label> <input type="checkbox" bind:checked={separateInactive} /> {label} </label>

View File

@ -6,9 +6,14 @@
@typescript-eslint/no-explicit-any: "off", @typescript-eslint/no-explicit-any: "off",
*/ */
import { CardQueue } from "anki/cards"; import { CardQueue, CardType } from "anki/cards";
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import { schemeGreens, schemeBlues } from "d3-scale-chromatic"; import {
schemeGreens,
schemeBlues,
schemeOranges,
schemeReds,
} from "d3-scale-chromatic";
import "d3-transition"; import "d3-transition";
import { select } from "d3-selection"; import { select } from "d3-selection";
import { scaleLinear } from "d3-scale"; import { scaleLinear } from "d3-scale";
@ -18,55 +23,94 @@ import type { GraphBounds } from "./graph-helpers";
import { cumsum } from "d3-array"; import { cumsum } from "d3-array";
import type { I18n } from "anki/i18n"; import type { I18n } from "anki/i18n";
type Count = [string, number]; type Count = [string, number, boolean];
export interface GraphData { export interface GraphData {
title: string; title: string;
counts: Count[]; counts: Count[];
totalCards: number; totalCards: number;
} }
export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): GraphData { const barColours = [
const totalCards = data.cards.length; schemeBlues[5][2] /* new */,
schemeOranges[5][2] /* learn */,
schemeReds[5][2] /* relearn */,
schemeGreens[5][2] /* young */,
schemeGreens[5][3] /* mature */,
"#FFDC41" /* suspended */,
"grey" /* buried */,
];
function countCards(
cards: pb.BackendProto.ICard[],
separateInactive: boolean,
i18n: I18n
): Count[] {
let newCards = 0; let newCards = 0;
let learn = 0;
let relearn = 0;
let young = 0; let young = 0;
let mature = 0; let mature = 0;
let suspended = 0; let suspended = 0;
let buried = 0; let buried = 0;
for (const card of data.cards as pb.BackendProto.Card[]) { for (const card of cards as pb.BackendProto.Card[]) {
switch (card.queue) { if (separateInactive) {
case CardQueue.New: switch (card.queue) {
case CardQueue.Suspended:
suspended += 1;
continue;
case CardQueue.SchedBuried:
case CardQueue.UserBuried:
buried += 1;
continue;
}
}
switch (card.ctype) {
case CardType.New:
newCards += 1; newCards += 1;
break; break;
case CardQueue.Review: case CardType.Learn:
if (card.interval >= 21) { learn += 1;
break;
case CardType.Review:
if (card.interval < 21) {
young += 1;
} else {
mature += 1; mature += 1;
break;
} }
// young falls through
case CardQueue.Learn:
case CardQueue.DayLearn:
case CardQueue.PreviewRepeat:
young += 1;
break; break;
case CardQueue.Suspended: case CardType.Relearn:
suspended += 1; relearn += 1;
break;
case CardQueue.SchedBuried:
case CardQueue.UserBuried:
buried += 1;
break; break;
} }
} }
const counts = [ const counts: Count[] = [
[i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards] as Count, [i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards, true],
[i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS), young] as Count, [i18n.tr(i18n.TR.STATISTICS_COUNTS_LEARNING_CARDS), learn, true],
[i18n.tr(i18n.TR.STATISTICS_COUNTS_MATURE_CARDS), mature] as Count, [i18n.tr(i18n.TR.STATISTICS_COUNTS_RELEARNING_CARDS), relearn, true],
[i18n.tr(i18n.TR.STATISTICS_COUNTS_SUSPENDED_CARDS), suspended] as Count, [i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS), young, true],
[i18n.tr(i18n.TR.STATISTICS_COUNTS_BURIED_CARDS), buried] as Count, [i18n.tr(i18n.TR.STATISTICS_COUNTS_MATURE_CARDS), mature, true],
[
i18n.tr(i18n.TR.STATISTICS_COUNTS_SUSPENDED_CARDS),
suspended,
separateInactive,
],
[i18n.tr(i18n.TR.STATISTICS_COUNTS_BURIED_CARDS), buried, separateInactive],
]; ];
return counts;
}
export function gatherData(
data: pb.BackendProto.GraphsOut,
separateInactive: boolean,
i18n: I18n
): GraphData {
const totalCards = data.cards.length;
const counts = countCards(data.cards, separateInactive, i18n);
return { return {
title: i18n.tr(i18n.TR.STATISTICS_COUNTS_TITLE), title: i18n.tr(i18n.TR.STATISTICS_COUNTS_TITLE),
counts, counts,
@ -82,27 +126,12 @@ interface Reviews {
early: number; early: number;
} }
function barColour(idx: number): string {
switch (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:
default:
return "grey";
}
}
export interface SummedDatum { export interface SummedDatum {
label: string; label: string;
// count of this particular item // count of this particular item
count: number; count: number;
idx: number; // show up in the table
show: boolean;
// running total // running total
total: number; total: number;
} }
@ -119,12 +148,13 @@ export function renderCards(
bounds: GraphBounds, bounds: GraphBounds,
sourceData: GraphData sourceData: GraphData
): TableDatum[] { ): TableDatum[] {
const summed = cumsum(sourceData.counts, (d) => d[1]); const summed = cumsum(sourceData.counts, (d: Count) => d[1]);
const data = Array.from(summed).map((n, idx) => { const data = Array.from(summed).map((n, idx) => {
const count = sourceData.counts[idx]; const count = sourceData.counts[idx];
return { return {
label: count[0], label: count[0],
count: count[1], count: count[1],
show: count[2],
idx, idx,
total: n, total: n,
} as SummedDatum; } as SummedDatum;
@ -135,7 +165,7 @@ export function renderCards(
const x = scaleLinear().domain([0, xMax]); const x = scaleLinear().domain([0, xMax]);
const svg = select(svgElem); const svg = select(svgElem);
const paths = svg.select(".counts"); const paths = svg.select(".counts");
const pieData = pie()(sourceData.counts.map((d) => d[1])); const pieData = pie()(sourceData.counts.map((d: Count) => d[1]));
const radius = bounds.height / 2 - bounds.marginTop - bounds.marginBottom; const radius = bounds.height / 2 - bounds.marginTop - bounds.marginBottom;
const arcGen = arc().innerRadius(0).outerRadius(radius); const arcGen = arc().innerRadius(0).outerRadius(radius);
const trans = svg.transition().duration(600) as any; const trans = svg.transition().duration(600) as any;
@ -148,8 +178,8 @@ export function renderCards(
(enter) => (enter) =>
enter enter
.append("path") .append("path")
.attr("fill", function (d, i) { .attr("fill", (_d, idx) => {
return barColour(i); return barColours[idx];
}) })
.attr("d", arcGen as any), .attr("d", arcGen as any),
function (update) { function (update) {
@ -167,14 +197,16 @@ export function renderCards(
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]); x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
const tableData = data.map((d, idx) => { const tableData = (data as any).flatMap((d: SummedDatum, idx: number) => {
const percent = ((d.count / xMax) * 100).toFixed(1); const percent = ((d.count / xMax) * 100).toFixed(1);
return { return d.show
label: d.label, ? ({
count: d.count, label: d.label,
percent: `${percent}%`, count: d.count,
colour: barColour(idx), percent: `${percent}%`,
} as TableDatum; colour: barColours[idx],
} as TableDatum)
: [];
}); });
return tableData; return tableData;

View File

@ -1,6 +1,13 @@
// 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
export enum CardType {
New = 0,
Learn = 1,
Review = 2,
Relearn = 3,
}
export enum CardQueue { export enum CardQueue {
/// due is the order cards are shown in /// due is the order cards are shown in
New = 0, New = 0,