mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
add an ease graph
This commit is contained in:
parent
e213ffc82a
commit
55ec4a2b82
7 changed files with 161 additions and 58 deletions
24
ts/src/stats/EaseGraph.svelte
Normal file
24
ts/src/stats/EaseGraph.svelte
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="typescript">
|
||||||
|
import { HistogramData } from "./histogram-graph";
|
||||||
|
import { gatherData, prepareData, GraphData } from "./ease";
|
||||||
|
import pb from "../backend/proto";
|
||||||
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
|
|
||||||
|
export let data: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
|
||||||
|
let svg = null as HTMLElement | SVGElement | null;
|
||||||
|
let histogramData = null as HistogramData | null;
|
||||||
|
|
||||||
|
$: if (data) {
|
||||||
|
console.log("gathering data");
|
||||||
|
histogramData = prepareData(gatherData(data));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if histogramData}
|
||||||
|
<div class="graph">
|
||||||
|
<h1>Card Ease</h1>
|
||||||
|
|
||||||
|
<HistogramGraph data={histogramData} xText="Ease (%)" yText="Number of cards" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -9,6 +9,7 @@
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import { getGraphData, GraphRange } from "./graphs";
|
import { getGraphData, GraphRange } from "./graphs";
|
||||||
import IntervalsGraph from "./IntervalsGraph.svelte";
|
import IntervalsGraph from "./IntervalsGraph.svelte";
|
||||||
|
import EaseGraph from "./EaseGraph.svelte";
|
||||||
|
|
||||||
let data: pb.BackendProto.GraphsOut | null = null;
|
let data: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
|
||||||
|
@ -107,3 +108,4 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IntervalsGraph {data} />
|
<IntervalsGraph {data} />
|
||||||
|
<EaseGraph {data} />
|
||||||
|
|
|
@ -27,7 +27,8 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph intervals">
|
{#if histogramData}
|
||||||
|
<div class="graph intervals">
|
||||||
<h1>Review Intervals</h1>
|
<h1>Review Intervals</h1>
|
||||||
|
|
||||||
<div class="range-box">
|
<div class="range-box">
|
||||||
|
@ -36,11 +37,17 @@
|
||||||
Month
|
Month
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={IntervalRange.Percentile50} />
|
<input
|
||||||
|
type="radio"
|
||||||
|
bind:group={range}
|
||||||
|
value={IntervalRange.Percentile50} />
|
||||||
50th percentile
|
50th percentile
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={IntervalRange.Percentile95} />
|
<input
|
||||||
|
type="radio"
|
||||||
|
bind:group={range}
|
||||||
|
value={IntervalRange.Percentile95} />
|
||||||
95th percentile
|
95th percentile
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
@ -60,4 +67,5 @@
|
||||||
data={histogramData}
|
data={histogramData}
|
||||||
xText="Interval (days)"
|
xText="Interval (days)"
|
||||||
yText="Number of cards" />
|
yText="Number of cards" />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
58
ts/src/stats/ease.ts
Normal file
58
ts/src/stats/ease.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
@typescript-eslint/no-explicit-any: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
|
import pb from "../backend/proto";
|
||||||
|
import { extent, histogram } from "d3-array";
|
||||||
|
import { scaleLinear, scaleSequential } from "d3-scale";
|
||||||
|
import { CardQueue } from "../cards";
|
||||||
|
import { HistogramData } from "./histogram-graph";
|
||||||
|
import { interpolateRdYlGn } from "d3-scale-chromatic";
|
||||||
|
|
||||||
|
export interface GraphData {
|
||||||
|
eases: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
||||||
|
const eases = (data.cards2 as pb.BackendProto.Card[])
|
||||||
|
.filter((c) => c.queue == CardQueue.Review)
|
||||||
|
.map((c) => c.factor / 10);
|
||||||
|
return { eases };
|
||||||
|
}
|
||||||
|
|
||||||
|
function hoverText(data: HistogramData, binIdx: number, _percent: number): string {
|
||||||
|
const bin = data.bins[binIdx];
|
||||||
|
const minPct = Math.floor(bin.x0!);
|
||||||
|
const maxPct = Math.floor(bin.x1!);
|
||||||
|
const ease = maxPct - minPct <= 10 ? `${bin.x0}%` : `${bin.x0}%~${bin.x1}%`;
|
||||||
|
|
||||||
|
return `${bin.length} cards with ${ease} ease.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareData(data: GraphData): HistogramData | null {
|
||||||
|
// get min/max
|
||||||
|
const allEases = data.eases;
|
||||||
|
if (!allEases.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const total = allEases.length;
|
||||||
|
const [_xMin, origXMax] = extent(allEases);
|
||||||
|
let xMax = origXMax;
|
||||||
|
const xMin = 130;
|
||||||
|
|
||||||
|
xMax = xMax! + 1;
|
||||||
|
const desiredBars = 20;
|
||||||
|
|
||||||
|
const scale = scaleLinear().domain([130, xMax!]).nice();
|
||||||
|
const bins = histogram()
|
||||||
|
.domain(scale.domain() as any)
|
||||||
|
.thresholds(scale.ticks(desiredBars))(allEases);
|
||||||
|
|
||||||
|
const colourScale = scaleSequential(interpolateRdYlGn).domain([xMin, 300]);
|
||||||
|
|
||||||
|
return { scale, bins, total, hoverText, colourScale, showArea: false };
|
||||||
|
}
|
|
@ -51,7 +51,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intervals .area {
|
.graph .area {
|
||||||
opacity: 0.05;
|
opacity: 0.05;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
fill: black;
|
fill: black;
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
import "d3-transition";
|
import "d3-transition";
|
||||||
import { select, mouse } from "d3-selection";
|
import { select, mouse } from "d3-selection";
|
||||||
import { cumsum, max, Bin } from "d3-array";
|
import { cumsum, max, Bin } from "d3-array";
|
||||||
import { interpolateBlues } from "d3-scale-chromatic";
|
import { interpolateBlues, interpolateRdYlGn } from "d3-scale-chromatic";
|
||||||
import { scaleLinear, scaleSequential, ScaleLinear } from "d3-scale";
|
import { scaleLinear, scaleSequential, ScaleLinear, ScaleSequential } from "d3-scale";
|
||||||
import { axisBottom, axisLeft } from "d3-axis";
|
import { axisBottom, axisLeft } from "d3-axis";
|
||||||
import { area } from "d3-shape";
|
import { area } from "d3-shape";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
|
@ -21,6 +21,8 @@ export interface HistogramData {
|
||||||
bins: Bin<number, number>[];
|
bins: Bin<number, number>[];
|
||||||
total: number;
|
total: number;
|
||||||
hoverText: (data: HistogramData, binIdx: number, percent: number) => string;
|
hoverText: (data: HistogramData, binIdx: number, percent: number) => string;
|
||||||
|
showArea: boolean;
|
||||||
|
colourScale: ScaleSequential<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function histogramGraph(
|
export function histogramGraph(
|
||||||
|
@ -57,16 +59,14 @@ export function histogramGraph(
|
||||||
return width ? width : 0;
|
return width ? width : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colour = scaleSequential(interpolateBlues).domain([-5, data.bins.length]);
|
|
||||||
|
|
||||||
const updateBar = (sel: any): any => {
|
const updateBar = (sel: any): any => {
|
||||||
return sel
|
return sel
|
||||||
.transition(trans)
|
|
||||||
.attr("width", barWidth)
|
.attr("width", barWidth)
|
||||||
|
.transition(trans)
|
||||||
.attr("x", (d: any) => x(d.x0))
|
.attr("x", (d: any) => x(d.x0))
|
||||||
.attr("y", (d: any) => y(d.length)!)
|
.attr("y", (d: any) => y(d.length)!)
|
||||||
.attr("height", (d: any) => y(0) - y(d.length))
|
.attr("height", (d: any) => y(0) - y(d.length))
|
||||||
.attr("fill", (d, idx) => colour(idx));
|
.attr("fill", (d, idx) => data.colourScale(d.x1));
|
||||||
};
|
};
|
||||||
|
|
||||||
svg.select("g.bars")
|
svg.select("g.bars")
|
||||||
|
@ -95,6 +95,7 @@ export function histogramGraph(
|
||||||
const areaData = cumsum(areaCounts);
|
const areaData = cumsum(areaCounts);
|
||||||
const yAreaScale = y.copy().domain([0, data.total]);
|
const yAreaScale = y.copy().domain([0, data.total]);
|
||||||
|
|
||||||
|
if (data.showArea && data.bins.length) {
|
||||||
svg.select("path.area")
|
svg.select("path.area")
|
||||||
.datum(areaData as any)
|
.datum(areaData as any)
|
||||||
.attr(
|
.attr(
|
||||||
|
@ -110,6 +111,7 @@ export function histogramGraph(
|
||||||
.y0(bounds.height - bounds.marginBottom)
|
.y0(bounds.height - bounds.marginBottom)
|
||||||
.y1((d: any) => yAreaScale(d)) as any
|
.y1((d: any) => yAreaScale(d)) as any
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// hover/tooltip
|
// hover/tooltip
|
||||||
svg.select("g.hoverzone")
|
svg.select("g.hoverzone")
|
||||||
|
@ -122,7 +124,7 @@ export function histogramGraph(
|
||||||
.attr("height", () => y(0) - y(yMax!))
|
.attr("height", () => y(0) - y(yMax!))
|
||||||
.on("mousemove", function (this: any, d: any, idx) {
|
.on("mousemove", function (this: any, d: any, idx) {
|
||||||
const [x, y] = mouse(document.body);
|
const [x, y] = mouse(document.body);
|
||||||
const pct = (areaData[idx + 1] / data.total) * 100;
|
const pct = data.showArea ? (areaData[idx + 1] / data.total) * 100 : 0;
|
||||||
showTooltip(data.hoverText(data, idx, pct), x, y);
|
showTooltip(data.hoverText(data, idx, pct), x, y);
|
||||||
})
|
})
|
||||||
.on("mouseout", hideTooltip);
|
.on("mouseout", hideTooltip);
|
||||||
|
|
|
@ -8,9 +8,10 @@
|
||||||
|
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import { extent, histogram, quantile } from "d3-array";
|
import { extent, histogram, quantile } from "d3-array";
|
||||||
import { scaleLinear } from "d3-scale";
|
import { scaleLinear, scaleSequential } from "d3-scale";
|
||||||
import { CardQueue } from "../cards";
|
import { CardQueue } from "../cards";
|
||||||
import { HistogramData } from "./histogram-graph";
|
import { HistogramData } from "./histogram-graph";
|
||||||
|
import { interpolateBlues } from "d3-scale-chromatic";
|
||||||
|
|
||||||
export interface IntervalGraphData {
|
export interface IntervalGraphData {
|
||||||
intervals: number[];
|
intervals: number[];
|
||||||
|
@ -46,9 +47,13 @@ function hoverText(data: HistogramData, binIdx: number, percent: number): string
|
||||||
export function prepareIntervalData(
|
export function prepareIntervalData(
|
||||||
data: IntervalGraphData,
|
data: IntervalGraphData,
|
||||||
range: IntervalRange
|
range: IntervalRange
|
||||||
): HistogramData {
|
): HistogramData | null {
|
||||||
// get min/max
|
// get min/max
|
||||||
const allIntervals = data.intervals;
|
const allIntervals = data.intervals;
|
||||||
|
if (!allIntervals.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const total = allIntervals.length;
|
const total = allIntervals.length;
|
||||||
const [xMin, origXMax] = extent(allIntervals);
|
const [xMin, origXMax] = extent(allIntervals);
|
||||||
let xMax = origXMax;
|
let xMax = origXMax;
|
||||||
|
@ -80,5 +85,9 @@ export function prepareIntervalData(
|
||||||
.domain(scale.domain() as any)
|
.domain(scale.domain() as any)
|
||||||
.thresholds(scale.ticks(desiredBars))(allIntervals);
|
.thresholds(scale.ticks(desiredBars))(allIntervals);
|
||||||
|
|
||||||
return { scale, bins, total, hoverText };
|
// start slightly darker
|
||||||
|
const shiftedMin = xMin! - Math.round((xMax - xMin!) / 10);
|
||||||
|
const colourScale = scaleSequential(interpolateBlues).domain([shiftedMin, xMax]);
|
||||||
|
|
||||||
|
return { scale, bins, total, hoverText, colourScale, showArea: true };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue