anki/ts/src/stats/card-counts.ts

181 lines
5.3 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 } from "../cards";
2020-07-04 05:38:46 +02:00
import pb from "../backend/proto";
import { schemeGreens, schemeBlues } from "d3-scale-chromatic";
import "d3-transition";
import { select, mouse } from "d3-selection";
import { scaleLinear } from "d3-scale";
import { showTooltip, hideTooltip } from "./tooltip";
import { GraphBounds } from "./graphs";
import { cumsum } from "d3-array";
2020-06-27 13:10:17 +02:00
import { I18n } from "../i18n";
2020-06-27 13:10:17 +02:00
type Count = [string, number];
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-07-04 05:38:46 +02:00
export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): GraphData {
// fixme: handle preview cards
const totalCards = data.cards.length;
let newCards = 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:
newCards += 1;
break;
case CardQueue.Review:
if (card.ivl >= 21) {
mature += 1;
break;
}
// young falls through
case CardQueue.Learn:
case CardQueue.DayLearn:
2020-07-30 01:10:40 +02:00
case CardQueue.PreviewRepeat:
young += 1;
break;
case CardQueue.Suspended:
suspended += 1;
break;
case CardQueue.SchedBuried:
case CardQueue.UserBuried:
buried += 1;
break;
}
}
2020-06-27 13:10:17 +02:00
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,
];
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-16 04:28:31 +02:00
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";
}
}
interface SummedDatum {
label: string;
// count of this particular item
2020-07-16 04:28:31 +02:00
count: number;
idx: number;
// running total
2020-07-16 04:28:31 +02:00
total: number;
}
2020-07-04 05:38:46 +02:00
export function renderCards(
svgElem: SVGElement,
bounds: GraphBounds,
sourceData: GraphData
): void {
const summed = cumsum(sourceData.counts, (d) => d[1]);
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],
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-07-06 06:01:49 +02:00
// ensuring a non-zero range makes a better animation
// in the empty data case
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);
const trans = svg.transition().duration(600) as any;
2020-07-06 06:01:49 +02:00
x.range([bounds.marginLeft, bounds.width - bounds.marginRight - bounds.marginLeft]);
2020-07-04 05:38:46 +02:00
2020-07-16 04:28:31 +02:00
const tooltipText = (current: SummedDatum): string => {
const rows: string[] = [];
2020-07-16 04:28:31 +02:00
for (const [idx, d] of data.entries()) {
const pct = ((d.count / xMax) * 100).toFixed(2);
const colour = `<span style="color: ${barColour(idx)};">■</span>`;
let label = `${colour} ${d.label}`;
2020-07-16 04:28:31 +02:00
if (idx === current.idx) {
label = `<b>${label}</b>`;
2020-07-16 04:28:31 +02:00
}
const count = d.count;
const pctStr = `${pct}%`;
const row = `<tr>
<td>${label}</td>
<td align=right>${count}</td>
<td align=right>${pctStr}</td>
</tr>`;
rows.push(row);
2020-07-16 04:28:31 +02:00
}
return `<table>${rows.join("")}</table>`;
};
2020-07-04 05:38:46 +02:00
const updateBar = (sel: any): any => {
return sel
2020-07-16 04:28:31 +02:00
.on("mousemove", function (this: any, d: SummedDatum) {
2020-07-04 05:38:46 +02:00
const [x, y] = mouse(document.body);
showTooltip(tooltipText(d), x, y);
})
.transition(trans)
2020-07-16 04:28:31 +02:00
.attr("x", (d: SummedDatum) => x(d.total - d.count))
.attr("width", (d: SummedDatum) => x(d.count) - x(0));
2020-07-04 05:38:46 +02:00
};
svg.select("g.days")
.selectAll("rect")
.data(data)
.join(
(enter) =>
enter
.append("rect")
.attr("height", 10)
.attr("y", bounds.marginTop)
2020-07-16 04:28:31 +02:00
.attr("fill", (d: SummedDatum): any => barColour(d.idx))
2020-07-04 05:38:46 +02:00
.on("mouseout", hideTooltip)
.call((d) => updateBar(d)),
(update) => update.call((d) => updateBar(d))
);
}