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:
commit
844b725809
@ -87,6 +87,7 @@ statistics-counts-early-cards = Early
|
||||
statistics-counts-learning-cards = Learning
|
||||
statistics-counts-relearning-cards = Relearning
|
||||
statistics-counts-title = Card Counts
|
||||
statistics-counts-separate-suspended-buried-cards = Separate suspended / buried cards
|
||||
statistics-range-all-time = all
|
||||
statistics-range-1-year-history = last 12 months
|
||||
statistics-range-all-history = all history
|
||||
|
@ -4,10 +4,12 @@
|
||||
import type { GraphData, TableDatum } from "./card-counts";
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
import SeparateInactiveCheckbox from "./SeparateInactiveCheckbox.svelte";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut;
|
||||
export let i18n: I18n;
|
||||
|
||||
let separateInactive = false;
|
||||
let svg = null as HTMLElement | SVGElement | null;
|
||||
|
||||
let bounds = defaultGraphBounds();
|
||||
@ -16,8 +18,9 @@
|
||||
|
||||
let graphData = (null as unknown) as GraphData;
|
||||
let tableData = (null as unknown) as TableDatum[];
|
||||
|
||||
$: {
|
||||
graphData = gatherData(sourceData, i18n);
|
||||
graphData = gatherData(sourceData, separateInactive, i18n);
|
||||
tableData = renderCards(svg as any, bounds, graphData);
|
||||
}
|
||||
|
||||
@ -52,6 +55,10 @@
|
||||
<div class="graph" id="graph-card-counts">
|
||||
<h1>{graphData.title}</h1>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<SeparateInactiveCheckbox bind:separateInactive {i18n} />
|
||||
</div>
|
||||
|
||||
<div class="counts-outer">
|
||||
<svg
|
||||
bind:this={svg}
|
||||
|
10
ts/graphs/SeparateInactiveCheckbox.svelte
Normal file
10
ts/graphs/SeparateInactiveCheckbox.svelte
Normal 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>
|
@ -6,9 +6,14 @@
|
||||
@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 { schemeGreens, schemeBlues } from "d3-scale-chromatic";
|
||||
import {
|
||||
schemeGreens,
|
||||
schemeBlues,
|
||||
schemeOranges,
|
||||
schemeReds,
|
||||
} from "d3-scale-chromatic";
|
||||
import "d3-transition";
|
||||
import { select } from "d3-selection";
|
||||
import { scaleLinear } from "d3-scale";
|
||||
@ -18,55 +23,94 @@ import type { GraphBounds } from "./graph-helpers";
|
||||
import { cumsum } from "d3-array";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
type Count = [string, number];
|
||||
type Count = [string, number, boolean];
|
||||
export interface GraphData {
|
||||
title: string;
|
||||
counts: Count[];
|
||||
totalCards: number;
|
||||
}
|
||||
|
||||
export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): GraphData {
|
||||
const totalCards = data.cards.length;
|
||||
const barColours = [
|
||||
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 learn = 0;
|
||||
let relearn = 0;
|
||||
let young = 0;
|
||||
let mature = 0;
|
||||
let suspended = 0;
|
||||
let buried = 0;
|
||||
|
||||
for (const card of data.cards as pb.BackendProto.Card[]) {
|
||||
switch (card.queue) {
|
||||
case CardQueue.New:
|
||||
for (const card of cards as pb.BackendProto.Card[]) {
|
||||
if (separateInactive) {
|
||||
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;
|
||||
break;
|
||||
case CardQueue.Review:
|
||||
if (card.interval >= 21) {
|
||||
case CardType.Learn:
|
||||
learn += 1;
|
||||
break;
|
||||
case CardType.Review:
|
||||
if (card.interval < 21) {
|
||||
young += 1;
|
||||
} else {
|
||||
mature += 1;
|
||||
break;
|
||||
}
|
||||
// young falls through
|
||||
case CardQueue.Learn:
|
||||
case CardQueue.DayLearn:
|
||||
case CardQueue.PreviewRepeat:
|
||||
young += 1;
|
||||
break;
|
||||
case CardQueue.Suspended:
|
||||
suspended += 1;
|
||||
break;
|
||||
case CardQueue.SchedBuried:
|
||||
case CardQueue.UserBuried:
|
||||
buried += 1;
|
||||
case CardType.Relearn:
|
||||
relearn += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const counts = [
|
||||
[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_MATURE_CARDS), mature] as Count,
|
||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_SUSPENDED_CARDS), suspended] as Count,
|
||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_BURIED_CARDS), buried] as Count,
|
||||
const counts: Count[] = [
|
||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards, true],
|
||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_LEARNING_CARDS), learn, true],
|
||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_RELEARNING_CARDS), relearn, true],
|
||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS), young, true],
|
||||
[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 {
|
||||
title: i18n.tr(i18n.TR.STATISTICS_COUNTS_TITLE),
|
||||
counts,
|
||||
@ -82,27 +126,12 @@ interface Reviews {
|
||||
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 {
|
||||
label: string;
|
||||
// count of this particular item
|
||||
count: number;
|
||||
idx: number;
|
||||
// show up in the table
|
||||
show: boolean;
|
||||
// running total
|
||||
total: number;
|
||||
}
|
||||
@ -119,12 +148,13 @@ export function renderCards(
|
||||
bounds: GraphBounds,
|
||||
sourceData: GraphData
|
||||
): 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 count = sourceData.counts[idx];
|
||||
return {
|
||||
label: count[0],
|
||||
count: count[1],
|
||||
show: count[2],
|
||||
idx,
|
||||
total: n,
|
||||
} as SummedDatum;
|
||||
@ -135,7 +165,7 @@ export function renderCards(
|
||||
const x = scaleLinear().domain([0, xMax]);
|
||||
const svg = select(svgElem);
|
||||
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 arcGen = arc().innerRadius(0).outerRadius(radius);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
@ -148,8 +178,8 @@ export function renderCards(
|
||||
(enter) =>
|
||||
enter
|
||||
.append("path")
|
||||
.attr("fill", function (d, i) {
|
||||
return barColour(i);
|
||||
.attr("fill", (_d, idx) => {
|
||||
return barColours[idx];
|
||||
})
|
||||
.attr("d", arcGen as any),
|
||||
function (update) {
|
||||
@ -167,14 +197,16 @@ export function renderCards(
|
||||
|
||||
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);
|
||||
return {
|
||||
label: d.label,
|
||||
count: d.count,
|
||||
percent: `${percent}%`,
|
||||
colour: barColour(idx),
|
||||
} as TableDatum;
|
||||
return d.show
|
||||
? ({
|
||||
label: d.label,
|
||||
count: d.count,
|
||||
percent: `${percent}%`,
|
||||
colour: barColours[idx],
|
||||
} as TableDatum)
|
||||
: [];
|
||||
});
|
||||
|
||||
return tableData;
|
||||
|
@ -1,6 +1,13 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 {
|
||||
/// due is the order cards are shown in
|
||||
New = 0,
|
||||
|
Loading…
Reference in New Issue
Block a user