add an ease graph

This commit is contained in:
Damien Elmes 2020-06-23 20:43:19 +10:00
parent e213ffc82a
commit 55ec4a2b82
7 changed files with 161 additions and 58 deletions

View 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}

View file

@ -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} />

View file

@ -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
View 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 };
}

View file

@ -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;

View file

@ -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);

View file

@ -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 };
} }