anki/ts/graphs/card-counts.ts

242 lines
7.0 KiB
TypeScript
Raw Normal View History

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
2020-07-04 05:38:46 +02:00
/* eslint
@typescript-eslint/no-non-null-assertion: "off",
@typescript-eslint/no-explicit-any: "off",
*/
import { CardQueue, CardType } from "anki/cards";
import type pb from "anki/backend_proto";
2021-01-04 15:36:15 +01:00
import { schemeGreens, schemeBlues, schemeOranges } from "d3-scale-chromatic";
2020-07-04 05:38:46 +02:00
import "d3-transition";
import { select } from "d3-selection";
2020-07-04 05:38:46 +02:00
import { scaleLinear } from "d3-scale";
2020-08-12 10:58:21 +02:00
import { pie, arc } from "d3-shape";
import { interpolate } from "d3-interpolate";
import type { GraphBounds } from "./graph-helpers";
import { CardCountMethod } from "./graph-helpers";
2020-07-04 05:38:46 +02:00
import { cumsum } from "d3-array";
import type { I18n } from "anki/i18n";
2021-01-04 15:14:50 +01:00
type Count = [string, number, string];
2020-07-04 05:38:46 +02:00
export interface GraphData {
2020-06-27 13:10:17 +02:00
title: string;
counts: Count[];
2020-07-04 05:38:46 +02:00
totalCards: number;
}
2021-01-04 15:14:50 +01:00
const barColours = {
new: schemeBlues[5][2],
review: schemeGreens[5][2],
young: schemeGreens[5][2],
mature: schemeGreens[5][3],
learn: schemeOranges[5][2],
relearn: schemeOranges[5][3],
suspended: "#FFDC41",
buried: "grey",
2021-01-04 15:36:15 +01:00
};
2021-01-04 15:14:50 +01:00
function gatherByQueue(cards: pb.BackendProto.ICard[], i18n: I18n): Count[] {
let newCards = 0;
let learn = 0;
let review = 0;
let suspended = 0;
let buried = 0;
for (const card of cards as pb.BackendProto.Card[]) {
switch (card.queue) {
case CardQueue.New:
newCards += 1;
break;
case CardQueue.Review:
review += 1;
2021-01-04 15:36:15 +01:00
break;
case CardQueue.Learn:
case CardQueue.DayLearn:
2020-07-30 01:10:40 +02:00
case CardQueue.PreviewRepeat:
learn += 1;
break;
case CardQueue.Suspended:
suspended += 1;
break;
case CardQueue.SchedBuried:
case CardQueue.UserBuried:
buried += 1;
break;
}
}
const counts: Count[] = [
2021-01-04 15:14:50 +01:00
[i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards, barColours.new],
[i18n.tr(i18n.TR.STATISTICS_COUNTS_LEARNING_CARDS), learn, barColours.learn],
["Review", review, barColours.review],
2021-01-04 15:36:15 +01:00
[
i18n.tr(i18n.TR.STATISTICS_COUNTS_SUSPENDED_CARDS),
suspended,
barColours.suspended,
],
2021-01-04 15:14:50 +01:00
[i18n.tr(i18n.TR.STATISTICS_COUNTS_BURIED_CARDS), buried, barColours.buried],
];
return counts;
}
function gatherByCtype(cards: pb.BackendProto.ICard[], i18n: I18n): Count[] {
let newCards = 0;
let learn = 0;
let young = 0;
let mature = 0;
let relearn = 0;
for (const card of cards as pb.BackendProto.Card[]) {
switch (card.ctype) {
case CardType.New:
newCards += 1;
break;
case CardType.Learn:
learn += 1;
break;
case CardType.Review:
if (card.interval < 21) {
young += 1;
2021-01-04 15:36:15 +01:00
} else {
mature += 1;
}
2021-01-04 15:36:15 +01:00
break;
case CardType.Relearn:
relearn += 1;
2021-01-04 15:36:15 +01:00
break;
}
}
const counts: Count[] = [
2021-01-04 15:14:50 +01:00
[i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards, barColours.new],
[i18n.tr(i18n.TR.STATISTICS_COUNTS_LEARNING_CARDS), learn, barColours.learn],
[i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS), young, barColours.young],
[i18n.tr(i18n.TR.STATISTICS_COUNTS_MATURE_CARDS), mature, barColours.mature],
2021-01-04 15:36:15 +01:00
[
i18n.tr(i18n.TR.STATISTICS_COUNTS_RELEARNING_CARDS),
relearn,
barColours.relearn,
],
2020-06-27 13:10:17 +02:00
];
return counts;
}
2021-01-04 15:36:15 +01:00
export function gatherData(
data: pb.BackendProto.GraphsOut,
method: CardCountMethod,
i18n: I18n
): GraphData {
const totalCards = data.cards.length;
2021-01-04 15:36:15 +01:00
const counts =
method === CardCountMethod.ByType
? gatherByCtype(data.cards, i18n)
: gatherByQueue(data.cards, i18n);
return {
2020-06-27 13:10:17 +02:00
title: i18n.tr(i18n.TR.STATISTICS_COUNTS_TITLE),
counts,
2020-07-04 05:38:46 +02:00
totalCards,
};
}
interface Reviews {
mature: number;
young: number;
learn: number;
relearn: number;
early: number;
}
export interface SummedDatum {
2020-07-16 04:28:31 +02:00
label: string;
// count of this particular item
2020-07-16 04:28:31 +02:00
count: number;
2021-01-04 15:14:50 +01:00
colour: string;
// running total
2020-07-16 04:28:31 +02:00
total: number;
}
export interface TableDatum {
label: string;
count: number;
percent: string;
colour: string;
}
2020-07-04 05:38:46 +02:00
export function renderCards(
svgElem: SVGElement,
bounds: GraphBounds,
2020-08-12 10:58:21 +02:00
sourceData: GraphData
): TableDatum[] {
2021-01-04 15:14:50 +01:00
const summed = cumsum(sourceData.counts, (d: Count) => d[1]);
2020-07-04 05:38:46 +02:00
const data = Array.from(summed).map((n, idx) => {
2020-07-16 04:28:31 +02:00
const count = sourceData.counts[idx];
2020-07-04 05:38:46 +02:00
return {
2020-07-16 04:28:31 +02:00
label: count[0],
count: count[1],
2021-01-04 15:14:50 +01:00
colour: count[2],
2020-07-04 05:38:46 +02:00
idx,
total: n,
2020-07-16 04:28:31 +02:00
} as SummedDatum;
2020-07-04 05:38:46 +02:00
});
2020-08-12 10:58:21 +02:00
// ensuring a non-zero range makes the percentages not break
// in an empty collection
2020-07-06 06:01:49 +02:00
const xMax = Math.max(1, summed.slice(-1)[0]);
2020-07-04 05:38:46 +02:00
const x = scaleLinear().domain([0, xMax]);
const svg = select(svgElem);
2020-08-12 10:58:21 +02:00
const paths = svg.select(".counts");
2021-01-04 15:14:50 +01:00
const pieData = pie()(sourceData.counts.map((d: Count) => d[1]));
2020-08-12 10:58:21 +02:00
const radius = bounds.height / 2 - bounds.marginTop - bounds.marginBottom;
const arcGen = arc().innerRadius(0).outerRadius(radius);
2020-07-04 05:38:46 +02:00
const trans = svg.transition().duration(600) as any;
2020-08-12 10:58:21 +02:00
paths
.attr("transform", `translate(${radius},${radius + bounds.marginTop})`)
.selectAll("path")
.data(pieData)
.join(
(enter) =>
enter
.append("path")
2021-01-04 15:14:50 +01:00
.attr("fill", (_d, i) => {
return data[i].colour;
2020-08-12 10:58:21 +02:00
})
.attr("d", arcGen as any),
function (update) {
return update.call((d) =>
2021-01-04 15:14:50 +01:00
d
2021-01-04 15:36:15 +01:00
.transition(trans)
.attr("fill", (_d, i) => {
return data[i].colour;
})
.attrTween("d", (d) => {
const interpolator = interpolate(
{ startAngle: 0, endAngle: 0 },
d
);
return (t): string =>
arcGen(interpolator(t) as any) as string;
})
2020-08-12 10:58:21 +02:00
);
}
);
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
2020-07-04 05:38:46 +02:00
2021-01-04 15:14:50 +01:00
const tableData = data.map((d) => {
const percent = ((d.count / xMax) * 100).toFixed(1);
return {
label: d.label,
count: d.count,
percent: `${percent}%`,
2021-01-04 15:14:50 +01:00
colour: d.colour,
} as TableDatum;
});
2020-07-04 05:38:46 +02:00
return tableData;
}