anki/ts/graphs/card-counts.ts

229 lines
6.2 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-explicit-any: "off",
*/
2021-01-05 19:37:14 +01:00
import {
arc,
cumsum,
interpolate,
pie,
scaleLinear,
2021-01-05 19:37:14 +01:00
schemeBlues,
schemeGreens,
2021-01-05 19:37:14 +01:00
schemeOranges,
schemeReds,
select,
} from "d3";
import { CardQueue, CardType } from "../lib/cards";
import * as tr from "../lib/ftl";
import { localizedNumber } from "../lib/i18n";
import type { Cards, Stats } from "../lib/proto";
import type { GraphBounds } from "./graph-helpers";
type Count = [string, number, boolean, string];
2020-07-04 05:38:46 +02:00
export interface GraphData {
2020-06-27 13:10:17 +02:00
title: string;
counts: Count[];
totalCards: string;
}
const barColours = [
schemeBlues[5][2] /* new */,
schemeOranges[5][2] /* learn */,
2021-01-05 19:37:14 +01:00
schemeReds[5][2] /* relearn */,
schemeGreens[5][2] /* young */,
schemeGreens[5][3] /* mature */,
"#FFDC41" /* suspended */,
"grey" /* buried */,
];
2021-07-10 11:52:31 +02:00
function countCards(cards: Cards.ICard[], separateInactive: boolean): Count[] {
let newCards = 0;
let learn = 0;
let relearn = 0;
let young = 0;
let mature = 0;
let suspended = 0;
let buried = 0;
2021-07-10 11:52:31 +02:00
for (const card of cards as Cards.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 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;
}
}
2021-01-08 14:28:38 +01:00
const extraQuery = separateInactive ? 'AND -("is:buried" OR "is:suspended")' : "";
const counts: Count[] = [
[tr.statisticsCountsNewCards(), newCards, true, `"is:new"${extraQuery}`],
[
tr.statisticsCountsLearningCards(),
learn,
true,
2021-01-08 14:28:38 +01:00
`(-"is:review" AND "is:learn")${extraQuery}`,
],
[
tr.statisticsCountsRelearningCards(),
relearn,
true,
2021-01-08 14:28:38 +01:00
`("is:review" AND "is:learn")${extraQuery}`,
],
[
tr.statisticsCountsYoungCards(),
young,
true,
2021-01-08 14:28:38 +01:00
`("is:review" AND -"is:learn") AND "prop:ivl<21"${extraQuery}`,
],
[
tr.statisticsCountsMatureCards(),
mature,
true,
2021-01-08 14:28:38 +01:00
`("is:review" -"is:learn") AND "prop:ivl>=21"${extraQuery}`,
],
2021-01-05 17:15:47 +01:00
[
tr.statisticsCountsSuspendedCards(),
2021-01-05 17:15:47 +01:00
suspended,
separateInactive,
2021-01-08 14:28:38 +01:00
'"is:suspended"',
],
[tr.statisticsCountsBuriedCards(), buried, separateInactive, '"is:buried"'],
2021-01-05 17:15:47 +01:00
];
return counts;
}
2021-01-04 15:36:15 +01:00
export function gatherData(
data: Stats.GraphsResponse,
separateInactive: boolean,
2021-01-04 15:36:15 +01:00
): GraphData {
const totalCards = localizedNumber(data.cards.length);
const counts = countCards(data.cards, separateInactive);
return {
title: tr.statisticsCountsTitle(),
2020-06-27 13:10:17 +02:00
counts,
2020-07-04 05:38:46 +02:00
totalCards,
};
}
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;
// show up in the table
2021-01-05 17:15:47 +01:00
show: boolean;
query: string;
// running total
2020-07-16 04:28:31 +02:00
total: number;
}
export interface TableDatum {
label: string;
count: string;
query: string;
percent: string;
colour: string;
}
2020-07-04 05:38:46 +02:00
export function renderCards(
svgElem: SVGElement,
bounds: GraphBounds,
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],
show: count[2],
query: count[3],
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(
2021-01-05 17:15:47 +01:00
(enter) =>
enter
2020-08-12 10:58:21 +02:00
.append("path")
.attr("fill", (_d, idx) => {
return barColours[idx];
2020-08-12 10:58:21 +02:00
})
.attr("d", arcGen as any),
function (update) {
return update.call((d) =>
2021-01-05 17:15:47 +01:00
d.transition(trans).attrTween("d", (d) => {
const interpolator = interpolate(
{ startAngle: 0, endAngle: 0 },
d,
2021-01-05 17:15:47 +01:00
);
return (t): string => arcGen(interpolator(t) as any) as string;
}),
2020-08-12 10:58:21 +02:00
);
},
2020-08-12 10:58:21 +02:00
);
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
2020-07-04 05:38:46 +02:00
const tableData = data.flatMap((d: SummedDatum, idx: number) => {
const percent = localizedNumber((d.count / xMax) * 100, 2);
return d.show
2021-01-05 17:15:47 +01:00
? ({
label: d.label,
count: localizedNumber(d.count),
2021-01-05 17:15:47 +01:00
percent: `${percent}%`,
colour: barColours[idx],
query: d.query,
2021-01-05 17:15:47 +01:00
} as TableDatum)
: [];
});
2020-07-04 05:38:46 +02:00
return tableData;
}