diff --git a/ts/src/stats/EaseGraph.svelte b/ts/src/stats/EaseGraph.svelte
new file mode 100644
index 000000000..dc07bfa5f
--- /dev/null
+++ b/ts/src/stats/EaseGraph.svelte
@@ -0,0 +1,24 @@
+
+
+{#if histogramData}
+
-
Review Intervals
+{#if histogramData}
+
+
Review Intervals
-
-
-
-
+{/if}
diff --git a/ts/src/stats/ease.ts b/ts/src/stats/ease.ts
new file mode 100644
index 000000000..6d8a8171c
--- /dev/null
+++ b/ts/src/stats/ease.ts
@@ -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 };
+}
diff --git a/ts/src/stats/graphs.css b/ts/src/stats/graphs.css
index 74acbba64..82fc26560 100644
--- a/ts/src/stats/graphs.css
+++ b/ts/src/stats/graphs.css
@@ -51,7 +51,7 @@
justify-content: center;
}
-.intervals .area {
+.graph .area {
opacity: 0.05;
pointer-events: none;
fill: black;
diff --git a/ts/src/stats/histogram-graph.ts b/ts/src/stats/histogram-graph.ts
index 3bd710457..70a5bdc84 100644
--- a/ts/src/stats/histogram-graph.ts
+++ b/ts/src/stats/histogram-graph.ts
@@ -9,8 +9,8 @@
import "d3-transition";
import { select, mouse } from "d3-selection";
import { cumsum, max, Bin } from "d3-array";
-import { interpolateBlues } from "d3-scale-chromatic";
-import { scaleLinear, scaleSequential, ScaleLinear } from "d3-scale";
+import { interpolateBlues, interpolateRdYlGn } from "d3-scale-chromatic";
+import { scaleLinear, scaleSequential, ScaleLinear, ScaleSequential } from "d3-scale";
import { axisBottom, axisLeft } from "d3-axis";
import { area } from "d3-shape";
import { showTooltip, hideTooltip } from "./tooltip";
@@ -21,6 +21,8 @@ export interface HistogramData {
bins: Bin
[];
total: number;
hoverText: (data: HistogramData, binIdx: number, percent: number) => string;
+ showArea: boolean;
+ colourScale: ScaleSequential;
}
export function histogramGraph(
@@ -57,16 +59,14 @@ export function histogramGraph(
return width ? width : 0;
}
- const colour = scaleSequential(interpolateBlues).domain([-5, data.bins.length]);
-
const updateBar = (sel: any): any => {
return sel
- .transition(trans)
.attr("width", barWidth)
+ .transition(trans)
.attr("x", (d: any) => x(d.x0))
.attr("y", (d: any) => 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")
@@ -95,21 +95,23 @@ export function histogramGraph(
const areaData = cumsum(areaCounts);
const yAreaScale = y.copy().domain([0, data.total]);
- svg.select("path.area")
- .datum(areaData as any)
- .attr(
- "d",
- area()
- .x((d, idx) => {
- if (idx === 0) {
- return x(data.bins[0].x0!);
- } else {
- return x(data.bins[idx - 1].x1!);
- }
- })
- .y0(bounds.height - bounds.marginBottom)
- .y1((d: any) => yAreaScale(d)) as any
- );
+ if (data.showArea && data.bins.length) {
+ svg.select("path.area")
+ .datum(areaData as any)
+ .attr(
+ "d",
+ area()
+ .x((d, idx) => {
+ if (idx === 0) {
+ return x(data.bins[0].x0!);
+ } else {
+ return x(data.bins[idx - 1].x1!);
+ }
+ })
+ .y0(bounds.height - bounds.marginBottom)
+ .y1((d: any) => yAreaScale(d)) as any
+ );
+ }
// hover/tooltip
svg.select("g.hoverzone")
@@ -122,7 +124,7 @@ export function histogramGraph(
.attr("height", () => y(0) - y(yMax!))
.on("mousemove", function (this: any, d: any, idx) {
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);
})
.on("mouseout", hideTooltip);
diff --git a/ts/src/stats/intervals.ts b/ts/src/stats/intervals.ts
index ed4da35c8..b9875787e 100644
--- a/ts/src/stats/intervals.ts
+++ b/ts/src/stats/intervals.ts
@@ -8,9 +8,10 @@
import pb from "../backend/proto";
import { extent, histogram, quantile } from "d3-array";
-import { scaleLinear } from "d3-scale";
+import { scaleLinear, scaleSequential } from "d3-scale";
import { CardQueue } from "../cards";
import { HistogramData } from "./histogram-graph";
+import { interpolateBlues } from "d3-scale-chromatic";
export interface IntervalGraphData {
intervals: number[];
@@ -46,9 +47,13 @@ function hoverText(data: HistogramData, binIdx: number, percent: number): string
export function prepareIntervalData(
data: IntervalGraphData,
range: IntervalRange
-): HistogramData {
+): HistogramData | null {
// get min/max
const allIntervals = data.intervals;
+ if (!allIntervals.length) {
+ return null;
+ }
+
const total = allIntervals.length;
const [xMin, origXMax] = extent(allIntervals);
let xMax = origXMax;
@@ -80,5 +85,9 @@ export function prepareIntervalData(
.domain(scale.domain() as any)
.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 };
}