2020-06-26 02:42:10 +02:00
|
|
|
// 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",
|
|
|
|
*/
|
|
|
|
|
2021-01-04 14:04:51 +01:00
|
|
|
import { CardQueue, CardType } from "anki/cards";
|
2020-11-05 02:01:30 +01:00
|
|
|
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";
|
2020-07-31 09:19:31 +02:00
|
|
|
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";
|
2020-11-01 05:26:58 +01:00
|
|
|
import type { GraphBounds } from "./graph-helpers";
|
2021-01-04 14:04:51 +01:00
|
|
|
import { CardCountMethod } from "./graph-helpers";
|
2020-07-04 05:38:46 +02:00
|
|
|
import { cumsum } from "d3-array";
|
2020-11-05 02:01:30 +01:00
|
|
|
import type { I18n } from "anki/i18n";
|
2020-06-26 02:42:10 +02:00
|
|
|
|
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;
|
2020-06-26 02:42:10 +02:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2021-01-04 14:04:51 +01:00
|
|
|
function gatherByQueue(cards: pb.BackendProto.ICard[], i18n: I18n): Count[] {
|
2020-06-26 02:42:10 +02:00
|
|
|
let newCards = 0;
|
2021-01-04 14:04:51 +01:00
|
|
|
let learn = 0;
|
|
|
|
let review = 0;
|
2020-06-26 02:42:10 +02:00
|
|
|
let suspended = 0;
|
|
|
|
let buried = 0;
|
|
|
|
|
2021-01-04 14:04:51 +01:00
|
|
|
for (const card of cards as pb.BackendProto.Card[]) {
|
2020-06-26 02:42:10 +02:00
|
|
|
switch (card.queue) {
|
|
|
|
case CardQueue.New:
|
|
|
|
newCards += 1;
|
|
|
|
break;
|
|
|
|
case CardQueue.Review:
|
2021-01-04 14:04:51 +01:00
|
|
|
review += 1;
|
2021-01-04 15:36:15 +01:00
|
|
|
break;
|
2020-06-26 02:42:10 +02:00
|
|
|
case CardQueue.Learn:
|
|
|
|
case CardQueue.DayLearn:
|
2020-07-30 01:10:40 +02:00
|
|
|
case CardQueue.PreviewRepeat:
|
2021-01-04 14:04:51 +01:00
|
|
|
learn += 1;
|
2020-06-26 02:42:10 +02:00
|
|
|
break;
|
|
|
|
case CardQueue.Suspended:
|
|
|
|
suspended += 1;
|
|
|
|
break;
|
|
|
|
case CardQueue.SchedBuried:
|
|
|
|
case CardQueue.UserBuried:
|
|
|
|
buried += 1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-04 14:04:51 +01:00
|
|
|
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],
|
2021-01-04 14:04:51 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
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 {
|
2021-01-04 14:04:51 +01:00
|
|
|
mature += 1;
|
|
|
|
}
|
2021-01-04 15:36:15 +01:00
|
|
|
break;
|
2021-01-04 14:04:51 +01:00
|
|
|
case CardType.Relearn:
|
|
|
|
relearn += 1;
|
2021-01-04 15:36:15 +01:00
|
|
|
break;
|
2021-01-04 14:04:51 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
];
|
|
|
|
|
2021-01-04 14:04:51 +01:00
|
|
|
return counts;
|
|
|
|
}
|
|
|
|
|
2021-01-04 15:36:15 +01:00
|
|
|
export function gatherData(
|
|
|
|
data: pb.BackendProto.GraphsOut,
|
|
|
|
method: CardCountMethod,
|
|
|
|
i18n: I18n
|
|
|
|
): GraphData {
|
2021-01-04 14:04:51 +01:00
|
|
|
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);
|
2021-01-04 14:04:51 +01:00
|
|
|
|
2020-06-26 02:42:10 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-07-31 09:19:31 +02:00
|
|
|
export interface SummedDatum {
|
2020-07-16 04:28:31 +02:00
|
|
|
label: string;
|
2020-07-16 05:18:35 +02:00
|
|
|
// 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;
|
2020-07-16 05:18:35 +02:00
|
|
|
// running total
|
2020-07-16 04:28:31 +02:00
|
|
|
total: number;
|
|
|
|
}
|
|
|
|
|
2020-07-31 09:19:31 +02:00
|
|
|
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
|
2020-07-31 09:19:31 +02:00
|
|
|
): 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
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2020-08-10 07:31:42 +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) => {
|
2020-07-31 09:19:31 +02:00
|
|
|
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,
|
2020-07-31 09:19:31 +02:00
|
|
|
} as TableDatum;
|
|
|
|
});
|
2020-07-04 05:38:46 +02:00
|
|
|
|
2020-07-31 09:19:31 +02:00
|
|
|
return tableData;
|
2020-06-26 02:42:10 +02:00
|
|
|
}
|