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",
|
|
|
|
*/
|
|
|
|
|
2020-06-26 02:42:10 +02:00
|
|
|
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-26 02:42:10 +02:00
|
|
|
|
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-06-26 02:42:10 +02:00
|
|
|
}
|
|
|
|
|
2020-07-04 05:38:46 +02:00
|
|
|
export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): GraphData {
|
|
|
|
// fixme: handle preview cards
|
2020-06-26 02:42:10 +02:00
|
|
|
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:
|
2020-06-26 02:42:10 +02:00
|
|
|
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,
|
|
|
|
];
|
|
|
|
|
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-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;
|
2020-07-16 05:18:35 +02:00
|
|
|
// count of this particular item
|
2020-07-16 04:28:31 +02:00
|
|
|
count: number;
|
|
|
|
idx: number;
|
2020-07-16 05:18:35 +02:00
|
|
|
// 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 => {
|
2020-07-16 05:18:35 +02:00
|
|
|
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>`;
|
2020-07-16 05:18:35 +02:00
|
|
|
let label = `${colour} ${d.label}`;
|
2020-07-16 04:28:31 +02:00
|
|
|
if (idx === current.idx) {
|
2020-07-16 05:18:35 +02:00
|
|
|
label = `<b>${label}</b>`;
|
2020-07-16 04:28:31 +02:00
|
|
|
}
|
2020-07-16 05:18:35 +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
|
|
|
}
|
2020-07-16 05:18:35 +02:00
|
|
|
return `<table>${rows.join("")}</table>`;
|
2020-06-26 02:42:10 +02:00
|
|
|
};
|
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))
|
|
|
|
);
|
2020-06-26 02:42:10 +02:00
|
|
|
}
|