From 55ec4a2b825af21dd3cf789b39d1b36fad44e685 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 23 Jun 2020 20:43:19 +1000 Subject: [PATCH] add an ease graph --- ts/src/stats/EaseGraph.svelte | 24 ++++++++++ ts/src/stats/GraphsPage.svelte | 2 + ts/src/stats/IntervalsGraph.svelte | 72 +++++++++++++++++------------- ts/src/stats/ease.ts | 58 ++++++++++++++++++++++++ ts/src/stats/graphs.css | 2 +- ts/src/stats/histogram-graph.ts | 46 ++++++++++--------- ts/src/stats/intervals.ts | 15 +++++-- 7 files changed, 161 insertions(+), 58 deletions(-) create mode 100644 ts/src/stats/EaseGraph.svelte create mode 100644 ts/src/stats/ease.ts 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} +
+

Card Ease

+ + +
+{/if} diff --git a/ts/src/stats/GraphsPage.svelte b/ts/src/stats/GraphsPage.svelte index 3372a0e4f..6e8ca2305 100644 --- a/ts/src/stats/GraphsPage.svelte +++ b/ts/src/stats/GraphsPage.svelte @@ -9,6 +9,7 @@ import pb from "../backend/proto"; import { getGraphData, GraphRange } from "./graphs"; import IntervalsGraph from "./IntervalsGraph.svelte"; + import EaseGraph from "./EaseGraph.svelte"; let data: pb.BackendProto.GraphsOut | null = null; @@ -107,3 +108,4 @@ + diff --git a/ts/src/stats/IntervalsGraph.svelte b/ts/src/stats/IntervalsGraph.svelte index 77fbd93a8..c0c69d571 100644 --- a/ts/src/stats/IntervalsGraph.svelte +++ b/ts/src/stats/IntervalsGraph.svelte @@ -27,37 +27,45 @@ } -
-

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