Merge pull request #892 from hgiesel/cardcounts
Add "By card type" and "By scheduling queue" variants of Card Counts stats graph
This commit is contained in:
commit
844b725809
@ -87,6 +87,7 @@ statistics-counts-early-cards = Early
|
|||||||
statistics-counts-learning-cards = Learning
|
statistics-counts-learning-cards = Learning
|
||||||
statistics-counts-relearning-cards = Relearning
|
statistics-counts-relearning-cards = Relearning
|
||||||
statistics-counts-title = Card Counts
|
statistics-counts-title = Card Counts
|
||||||
|
statistics-counts-separate-suspended-buried-cards = Separate suspended / buried cards
|
||||||
statistics-range-all-time = all
|
statistics-range-all-time = all
|
||||||
statistics-range-1-year-history = last 12 months
|
statistics-range-1-year-history = last 12 months
|
||||||
statistics-range-all-history = all history
|
statistics-range-all-history = all history
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
import type { GraphData, TableDatum } from "./card-counts";
|
import type { GraphData, TableDatum } from "./card-counts";
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
import SeparateInactiveCheckbox from "./SeparateInactiveCheckbox.svelte";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut;
|
export let sourceData: pb.BackendProto.GraphsOut;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
|
||||||
|
let separateInactive = false;
|
||||||
let svg = null as HTMLElement | SVGElement | null;
|
let svg = null as HTMLElement | SVGElement | null;
|
||||||
|
|
||||||
let bounds = defaultGraphBounds();
|
let bounds = defaultGraphBounds();
|
||||||
@ -16,8 +18,9 @@
|
|||||||
|
|
||||||
let graphData = (null as unknown) as GraphData;
|
let graphData = (null as unknown) as GraphData;
|
||||||
let tableData = (null as unknown) as TableDatum[];
|
let tableData = (null as unknown) as TableDatum[];
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
graphData = gatherData(sourceData, i18n);
|
graphData = gatherData(sourceData, separateInactive, i18n);
|
||||||
tableData = renderCards(svg as any, bounds, graphData);
|
tableData = renderCards(svg as any, bounds, graphData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +55,10 @@
|
|||||||
<div class="graph" id="graph-card-counts">
|
<div class="graph" id="graph-card-counts">
|
||||||
<h1>{graphData.title}</h1>
|
<h1>{graphData.title}</h1>
|
||||||
|
|
||||||
|
<div class="range-box-inner">
|
||||||
|
<SeparateInactiveCheckbox bind:separateInactive {i18n} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="counts-outer">
|
<div class="counts-outer">
|
||||||
<svg
|
<svg
|
||||||
bind:this={svg}
|
bind:this={svg}
|
||||||
|
10
ts/graphs/SeparateInactiveCheckbox.svelte
Normal file
10
ts/graphs/SeparateInactiveCheckbox.svelte
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script lang="typescript">
|
||||||
|
import type { I18n } from "anki/i18n";
|
||||||
|
|
||||||
|
export let i18n: I18n;
|
||||||
|
export let separateInactive: boolean = false;
|
||||||
|
|
||||||
|
const label = i18n.tr(i18n.TR.STATISTICS_COUNTS_SEPARATE_SUSPENDED_BURIED_CARDS);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label> <input type="checkbox" bind:checked={separateInactive} /> {label} </label>
|
@ -6,9 +6,14 @@
|
|||||||
@typescript-eslint/no-explicit-any: "off",
|
@typescript-eslint/no-explicit-any: "off",
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CardQueue } from "anki/cards";
|
import { CardQueue, CardType } from "anki/cards";
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import { schemeGreens, schemeBlues } from "d3-scale-chromatic";
|
import {
|
||||||
|
schemeGreens,
|
||||||
|
schemeBlues,
|
||||||
|
schemeOranges,
|
||||||
|
schemeReds,
|
||||||
|
} from "d3-scale-chromatic";
|
||||||
import "d3-transition";
|
import "d3-transition";
|
||||||
import { select } from "d3-selection";
|
import { select } from "d3-selection";
|
||||||
import { scaleLinear } from "d3-scale";
|
import { scaleLinear } from "d3-scale";
|
||||||
@ -18,55 +23,94 @@ import type { GraphBounds } from "./graph-helpers";
|
|||||||
import { cumsum } from "d3-array";
|
import { cumsum } from "d3-array";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
|
||||||
type Count = [string, number];
|
type Count = [string, number, boolean];
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
title: string;
|
title: string;
|
||||||
counts: Count[];
|
counts: Count[];
|
||||||
totalCards: number;
|
totalCards: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): GraphData {
|
const barColours = [
|
||||||
const totalCards = data.cards.length;
|
schemeBlues[5][2] /* new */,
|
||||||
|
schemeOranges[5][2] /* learn */,
|
||||||
|
schemeReds[5][2] /* relearn */,
|
||||||
|
schemeGreens[5][2] /* young */,
|
||||||
|
schemeGreens[5][3] /* mature */,
|
||||||
|
"#FFDC41" /* suspended */,
|
||||||
|
"grey" /* buried */,
|
||||||
|
];
|
||||||
|
|
||||||
|
function countCards(
|
||||||
|
cards: pb.BackendProto.ICard[],
|
||||||
|
separateInactive: boolean,
|
||||||
|
i18n: I18n
|
||||||
|
): Count[] {
|
||||||
let newCards = 0;
|
let newCards = 0;
|
||||||
|
let learn = 0;
|
||||||
|
let relearn = 0;
|
||||||
let young = 0;
|
let young = 0;
|
||||||
let mature = 0;
|
let mature = 0;
|
||||||
let suspended = 0;
|
let suspended = 0;
|
||||||
let buried = 0;
|
let buried = 0;
|
||||||
|
|
||||||
for (const card of data.cards as pb.BackendProto.Card[]) {
|
for (const card of cards as pb.BackendProto.Card[]) {
|
||||||
switch (card.queue) {
|
if (separateInactive) {
|
||||||
case CardQueue.New:
|
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;
|
newCards += 1;
|
||||||
break;
|
break;
|
||||||
case CardQueue.Review:
|
case CardType.Learn:
|
||||||
if (card.interval >= 21) {
|
learn += 1;
|
||||||
|
break;
|
||||||
|
case CardType.Review:
|
||||||
|
if (card.interval < 21) {
|
||||||
|
young += 1;
|
||||||
|
} else {
|
||||||
mature += 1;
|
mature += 1;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
// young falls through
|
|
||||||
case CardQueue.Learn:
|
|
||||||
case CardQueue.DayLearn:
|
|
||||||
case CardQueue.PreviewRepeat:
|
|
||||||
young += 1;
|
|
||||||
break;
|
break;
|
||||||
case CardQueue.Suspended:
|
case CardType.Relearn:
|
||||||
suspended += 1;
|
relearn += 1;
|
||||||
break;
|
|
||||||
case CardQueue.SchedBuried:
|
|
||||||
case CardQueue.UserBuried:
|
|
||||||
buried += 1;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const counts = [
|
const counts: Count[] = [
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards] as Count,
|
[i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards, true],
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS), young] as Count,
|
[i18n.tr(i18n.TR.STATISTICS_COUNTS_LEARNING_CARDS), learn, true],
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_MATURE_CARDS), mature] as Count,
|
[i18n.tr(i18n.TR.STATISTICS_COUNTS_RELEARNING_CARDS), relearn, true],
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_SUSPENDED_CARDS), suspended] as Count,
|
[i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS), young, true],
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_BURIED_CARDS), buried] as Count,
|
[i18n.tr(i18n.TR.STATISTICS_COUNTS_MATURE_CARDS), mature, true],
|
||||||
|
[
|
||||||
|
i18n.tr(i18n.TR.STATISTICS_COUNTS_SUSPENDED_CARDS),
|
||||||
|
suspended,
|
||||||
|
separateInactive,
|
||||||
|
],
|
||||||
|
[i18n.tr(i18n.TR.STATISTICS_COUNTS_BURIED_CARDS), buried, separateInactive],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gatherData(
|
||||||
|
data: pb.BackendProto.GraphsOut,
|
||||||
|
separateInactive: boolean,
|
||||||
|
i18n: I18n
|
||||||
|
): GraphData {
|
||||||
|
const totalCards = data.cards.length;
|
||||||
|
const counts = countCards(data.cards, separateInactive, i18n);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: i18n.tr(i18n.TR.STATISTICS_COUNTS_TITLE),
|
title: i18n.tr(i18n.TR.STATISTICS_COUNTS_TITLE),
|
||||||
counts,
|
counts,
|
||||||
@ -82,27 +126,12 @@ interface Reviews {
|
|||||||
early: number;
|
early: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SummedDatum {
|
export interface SummedDatum {
|
||||||
label: string;
|
label: string;
|
||||||
// count of this particular item
|
// count of this particular item
|
||||||
count: number;
|
count: number;
|
||||||
idx: number;
|
// show up in the table
|
||||||
|
show: boolean;
|
||||||
// running total
|
// running total
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
@ -119,12 +148,13 @@ export function renderCards(
|
|||||||
bounds: GraphBounds,
|
bounds: GraphBounds,
|
||||||
sourceData: GraphData
|
sourceData: GraphData
|
||||||
): TableDatum[] {
|
): TableDatum[] {
|
||||||
const summed = cumsum(sourceData.counts, (d) => d[1]);
|
const summed = cumsum(sourceData.counts, (d: Count) => d[1]);
|
||||||
const data = Array.from(summed).map((n, idx) => {
|
const data = Array.from(summed).map((n, idx) => {
|
||||||
const count = sourceData.counts[idx];
|
const count = sourceData.counts[idx];
|
||||||
return {
|
return {
|
||||||
label: count[0],
|
label: count[0],
|
||||||
count: count[1],
|
count: count[1],
|
||||||
|
show: count[2],
|
||||||
idx,
|
idx,
|
||||||
total: n,
|
total: n,
|
||||||
} as SummedDatum;
|
} as SummedDatum;
|
||||||
@ -135,7 +165,7 @@ export function renderCards(
|
|||||||
const x = scaleLinear().domain([0, xMax]);
|
const x = scaleLinear().domain([0, xMax]);
|
||||||
const svg = select(svgElem);
|
const svg = select(svgElem);
|
||||||
const paths = svg.select(".counts");
|
const paths = svg.select(".counts");
|
||||||
const pieData = pie()(sourceData.counts.map((d) => d[1]));
|
const pieData = pie()(sourceData.counts.map((d: Count) => d[1]));
|
||||||
const radius = bounds.height / 2 - bounds.marginTop - bounds.marginBottom;
|
const radius = bounds.height / 2 - bounds.marginTop - bounds.marginBottom;
|
||||||
const arcGen = arc().innerRadius(0).outerRadius(radius);
|
const arcGen = arc().innerRadius(0).outerRadius(radius);
|
||||||
const trans = svg.transition().duration(600) as any;
|
const trans = svg.transition().duration(600) as any;
|
||||||
@ -148,8 +178,8 @@ export function renderCards(
|
|||||||
(enter) =>
|
(enter) =>
|
||||||
enter
|
enter
|
||||||
.append("path")
|
.append("path")
|
||||||
.attr("fill", function (d, i) {
|
.attr("fill", (_d, idx) => {
|
||||||
return barColour(i);
|
return barColours[idx];
|
||||||
})
|
})
|
||||||
.attr("d", arcGen as any),
|
.attr("d", arcGen as any),
|
||||||
function (update) {
|
function (update) {
|
||||||
@ -167,14 +197,16 @@ export function renderCards(
|
|||||||
|
|
||||||
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||||
|
|
||||||
const tableData = data.map((d, idx) => {
|
const tableData = (data as any).flatMap((d: SummedDatum, idx: number) => {
|
||||||
const percent = ((d.count / xMax) * 100).toFixed(1);
|
const percent = ((d.count / xMax) * 100).toFixed(1);
|
||||||
return {
|
return d.show
|
||||||
label: d.label,
|
? ({
|
||||||
count: d.count,
|
label: d.label,
|
||||||
percent: `${percent}%`,
|
count: d.count,
|
||||||
colour: barColour(idx),
|
percent: `${percent}%`,
|
||||||
} as TableDatum;
|
colour: barColours[idx],
|
||||||
|
} as TableDatum)
|
||||||
|
: [];
|
||||||
});
|
});
|
||||||
|
|
||||||
return tableData;
|
return tableData;
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
export enum CardType {
|
||||||
|
New = 0,
|
||||||
|
Learn = 1,
|
||||||
|
Review = 2,
|
||||||
|
Relearn = 3,
|
||||||
|
}
|
||||||
|
|
||||||
export enum CardQueue {
|
export enum CardQueue {
|
||||||
/// due is the order cards are shown in
|
/// due is the order cards are shown in
|
||||||
New = 0,
|
New = 0,
|
||||||
|
Loading…
Reference in New Issue
Block a user