mirror of
https://github.com/ankitects/anki.git
synced 2026-01-07 02:53:54 -05:00
Merge 0949555602 into 8f2144534b
This commit is contained in:
commit
faf1804052
8 changed files with 143 additions and 67 deletions
|
|
@ -17,7 +17,7 @@ impl GraphsContext {
|
||||||
if let Some(state) = card.memory_state {
|
if let Some(state) = card.memory_state {
|
||||||
*difficulty
|
*difficulty
|
||||||
.eases
|
.eases
|
||||||
.entry(percent_to_bin(state.difficulty() * 100.0))
|
.entry(percent_to_bin(state.difficulty() * 100.0, 1))
|
||||||
.or_insert_with(Default::default) += 1;
|
.or_insert_with(Default::default) += 1;
|
||||||
difficulty_values.push(state.difficulty());
|
difficulty_values.push(state.difficulty());
|
||||||
} else if matches!(card.ctype, CardType::Review | CardType::Relearn) {
|
} else if matches!(card.ctype, CardType::Review | CardType::Relearn) {
|
||||||
|
|
@ -51,11 +51,11 @@ fn median(data: &mut [f32]) -> f32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bins the number into a bin of 0, 5, .. 95
|
/// Bins the number into a bin of 0, 5, .. 95
|
||||||
pub(super) fn percent_to_bin(x: f32) -> u32 {
|
pub(super) fn percent_to_bin(x: f32, bin_size: u32) -> u32 {
|
||||||
if x == 100.0 {
|
if x == 100.0 {
|
||||||
95
|
100 - bin_size
|
||||||
} else {
|
} else {
|
||||||
((x / 5.0).floor() * 5.0) as u32
|
((x / bin_size as f32).floor() * bin_size as f32) as u32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,11 +65,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bins() {
|
fn bins() {
|
||||||
assert_eq!(percent_to_bin(0.0), 0);
|
assert_eq!(percent_to_bin(0.0, 5), 0);
|
||||||
assert_eq!(percent_to_bin(4.9), 0);
|
assert_eq!(percent_to_bin(4.9, 5), 0);
|
||||||
assert_eq!(percent_to_bin(5.0), 5);
|
assert_eq!(percent_to_bin(5.0, 5), 5);
|
||||||
assert_eq!(percent_to_bin(9.9), 5);
|
assert_eq!(percent_to_bin(9.9, 5), 5);
|
||||||
assert_eq!(percent_to_bin(99.9), 95);
|
assert_eq!(percent_to_bin(99.9, 5), 95);
|
||||||
assert_eq!(percent_to_bin(100.0), 95);
|
assert_eq!(percent_to_bin(100.0, 5), 95);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ impl GraphsContext {
|
||||||
|
|
||||||
*retrievability
|
*retrievability
|
||||||
.retrievability
|
.retrievability
|
||||||
.entry(percent_to_bin(r * 100.0))
|
.entry(percent_to_bin(r * 100.0, 1))
|
||||||
.or_insert_with(Default::default) += 1;
|
.or_insert_with(Default::default) += 1;
|
||||||
retrievability.sum_by_card += r;
|
retrievability.sum_by_card += r;
|
||||||
card_with_retrievability_count += 1;
|
card_with_retrievability_count += 1;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
import HistogramGraph from "./HistogramGraph.svelte";
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
import TableData from "./TableData.svelte";
|
import TableData from "./TableData.svelte";
|
||||||
|
import PercentageRange from "./PercentageRange.svelte";
|
||||||
|
import { PercentageRangeEnum, PercentageRangeToQuantile } from "./percentageRange";
|
||||||
|
|
||||||
export let sourceData: GraphsResponse | null = null;
|
export let sourceData: GraphsResponse | null = null;
|
||||||
export let prefs: GraphPrefs;
|
export let prefs: GraphPrefs;
|
||||||
|
|
@ -22,12 +24,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
let histogramData: HistogramData | null = null;
|
let histogramData: HistogramData | null = null;
|
||||||
let tableData: TableDatum[] = [];
|
let tableData: TableDatum[] = [];
|
||||||
|
let range = PercentageRangeEnum.All;
|
||||||
|
|
||||||
$: if (sourceData) {
|
$: if (sourceData) {
|
||||||
[histogramData, tableData] = prepareData(
|
[histogramData, tableData] = prepareData(
|
||||||
gatherData(sourceData),
|
gatherData(sourceData),
|
||||||
dispatch,
|
dispatch,
|
||||||
$prefs.browserLinksSupported,
|
$prefs.browserLinksSupported,
|
||||||
|
PercentageRangeToQuantile(range),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,6 +41,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
{#if sourceData?.fsrs}
|
{#if sourceData?.fsrs}
|
||||||
<Graph {title} {subtitle}>
|
<Graph {title} {subtitle}>
|
||||||
|
<PercentageRange bind:range />
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} />
|
<HistogramGraph data={histogramData} />
|
||||||
|
|
||||||
<TableData {tableData} />
|
<TableData {tableData} />
|
||||||
|
|
|
||||||
42
ts/routes/graphs/PercentageRange.svelte
Normal file
42
ts/routes/graphs/PercentageRange.svelte
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
|
import { PercentageRangeEnum } from "./percentageRange";
|
||||||
|
import * as tr from "@generated/ftl";
|
||||||
|
|
||||||
|
export let range: PercentageRangeEnum;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<InputBox>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
bind:group={range}
|
||||||
|
value={PercentageRangeEnum.Percentile50}
|
||||||
|
/>
|
||||||
|
50%
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
bind:group={range}
|
||||||
|
value={PercentageRangeEnum.Percentile95}
|
||||||
|
/>
|
||||||
|
95%
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
bind:group={range}
|
||||||
|
value={PercentageRangeEnum.Percentile100}
|
||||||
|
/>
|
||||||
|
100%
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={PercentageRangeEnum.All} />
|
||||||
|
{tr.statisticsRangeAllTime()}
|
||||||
|
</label>
|
||||||
|
</InputBox>
|
||||||
|
|
@ -14,6 +14,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import HistogramGraph from "./HistogramGraph.svelte";
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
import { gatherData, prepareData } from "./retrievability";
|
import { gatherData, prepareData } from "./retrievability";
|
||||||
import TableData from "./TableData.svelte";
|
import TableData from "./TableData.svelte";
|
||||||
|
import PercentageRange from "./PercentageRange.svelte";
|
||||||
|
import { PercentageRangeEnum, PercentageRangeToQuantile } from "./percentageRange";
|
||||||
|
|
||||||
export let sourceData: GraphsResponse | null = null;
|
export let sourceData: GraphsResponse | null = null;
|
||||||
export let prefs: GraphPrefs;
|
export let prefs: GraphPrefs;
|
||||||
|
|
@ -22,12 +24,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
let histogramData: HistogramData | null = null;
|
let histogramData: HistogramData | null = null;
|
||||||
let tableData: TableDatum[] = [];
|
let tableData: TableDatum[] = [];
|
||||||
|
let range = PercentageRangeEnum.All;
|
||||||
|
|
||||||
$: if (sourceData) {
|
$: if (sourceData) {
|
||||||
[histogramData, tableData] = prepareData(
|
[histogramData, tableData] = prepareData(
|
||||||
gatherData(sourceData),
|
gatherData(sourceData),
|
||||||
dispatch,
|
dispatch,
|
||||||
$prefs.browserLinksSupported,
|
$prefs.browserLinksSupported,
|
||||||
|
PercentageRangeToQuantile(range),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,6 +41,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
{#if sourceData?.fsrs}
|
{#if sourceData?.fsrs}
|
||||||
<Graph {title} {subtitle}>
|
<Graph {title} {subtitle}>
|
||||||
|
<PercentageRange bind:range />
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} />
|
<HistogramGraph data={histogramData} />
|
||||||
|
|
||||||
<TableData {tableData} />
|
<TableData {tableData} />
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,13 @@
|
||||||
import type { GraphsResponse } from "@generated/anki/stats_pb";
|
import type { GraphsResponse } from "@generated/anki/stats_pb";
|
||||||
import * as tr from "@generated/ftl";
|
import * as tr from "@generated/ftl";
|
||||||
import { localizedNumber } from "@tslib/i18n";
|
import { localizedNumber } from "@tslib/i18n";
|
||||||
import type { Bin, ScaleLinear } from "d3";
|
import type { Bin } from "d3";
|
||||||
import { bin, interpolateRdYlGn, scaleLinear, scaleSequential, sum } from "d3";
|
import { bin, interpolateRdYlGn, scaleSequential, sum } from "d3";
|
||||||
|
|
||||||
import type { SearchDispatch, TableDatum } from "./graph-helpers";
|
import type { SearchDispatch, TableDatum } from "./graph-helpers";
|
||||||
import { getNumericMapBinValue, numericMap } from "./graph-helpers";
|
import { getNumericMapBinValue, numericMap } from "./graph-helpers";
|
||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
|
import { getAdjustedScaleAndTicks, percentageRangeMinMax } from "./percentageRange";
|
||||||
|
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
eases: Map<number, number>;
|
eases: Map<number, number>;
|
||||||
|
|
@ -33,42 +34,18 @@ function makeQuery(start: number, end: number): string {
|
||||||
return `${fromQuery} AND ${tillQuery}`;
|
return `${fromQuery} AND ${tillQuery}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAdjustedScaleAndTicks(
|
|
||||||
min: number,
|
|
||||||
max: number,
|
|
||||||
desiredBars: number,
|
|
||||||
): [ScaleLinear<number, number, never>, number[]] {
|
|
||||||
const prescale = scaleLinear().domain([min, max]).nice();
|
|
||||||
const ticks = prescale.ticks(desiredBars);
|
|
||||||
|
|
||||||
const predomain = prescale.domain() as [number, number];
|
|
||||||
|
|
||||||
const minOffset = min - predomain[0];
|
|
||||||
const tickSize = ticks[1] - ticks[0];
|
|
||||||
|
|
||||||
if (minOffset === 0 || (minOffset % tickSize !== 0 && tickSize % minOffset !== 0)) {
|
|
||||||
return [prescale, ticks];
|
|
||||||
}
|
|
||||||
|
|
||||||
const add = (n: number): number => n + minOffset;
|
|
||||||
return [
|
|
||||||
scaleLinear().domain(predomain.map(add) as [number, number]),
|
|
||||||
ticks.map(add),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prepareData(
|
export function prepareData(
|
||||||
data: GraphData,
|
data: GraphData,
|
||||||
dispatch: SearchDispatch,
|
dispatch: SearchDispatch,
|
||||||
browserLinksSupported: boolean,
|
browserLinksSupported: boolean,
|
||||||
|
quantile?: number,
|
||||||
): [HistogramData | null, TableDatum[]] {
|
): [HistogramData | null, TableDatum[]] {
|
||||||
// get min/max
|
// get min/max
|
||||||
const allEases = data.eases;
|
const allEases = data.eases;
|
||||||
if (!allEases.size) {
|
if (!allEases.size) {
|
||||||
return [null, []];
|
return [null, []];
|
||||||
}
|
}
|
||||||
const xMin = 0;
|
const [xMin, xMax] = percentageRangeMinMax(allEases, quantile);
|
||||||
const xMax = 100;
|
|
||||||
const desiredBars = 20;
|
const desiredBars = 20;
|
||||||
|
|
||||||
const [scale, ticks] = getAdjustedScaleAndTicks(xMin, xMax, desiredBars);
|
const [scale, ticks] = getAdjustedScaleAndTicks(xMin, xMax, desiredBars);
|
||||||
|
|
|
||||||
68
ts/routes/graphs/percentageRange.ts
Normal file
68
ts/routes/graphs/percentageRange.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
|
||||||
|
import { range, type ScaleLinear, scaleLinear, sum } from "d3";
|
||||||
|
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export enum PercentageRangeEnum {
|
||||||
|
All = 0,
|
||||||
|
Percentile100 = 1,
|
||||||
|
Percentile95 = 2,
|
||||||
|
Percentile50 = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PercentageRangeToQuantile(range: PercentageRangeEnum) {
|
||||||
|
// These are halved because the quantiles are in both directions
|
||||||
|
return ({
|
||||||
|
[PercentageRangeEnum.Percentile100]: 1,
|
||||||
|
[PercentageRangeEnum.Percentile95]: 0.975,
|
||||||
|
[PercentageRangeEnum.Percentile50]: 0.75,
|
||||||
|
[PercentageRangeEnum.All]: undefined,
|
||||||
|
})[range];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function easeQuantile(data: Map<number, number>, quantile: number) {
|
||||||
|
let count = sum(data.values()) * quantile;
|
||||||
|
for (const [key, value] of data.entries()) {
|
||||||
|
count -= value;
|
||||||
|
if (count <= 0) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function percentageRangeMinMax(data: Map<number, number>, range: number | undefined) {
|
||||||
|
const xMin = range ? easeQuantile(data, 1 - range) ?? 0 : 0;
|
||||||
|
const xMax = range ? easeQuantile(data, range) ?? 0 : 100;
|
||||||
|
|
||||||
|
return [xMin, xMax];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAdjustedScaleAndTicks(
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
desiredBars: number,
|
||||||
|
): [ScaleLinear<number, number, never>, number[]] {
|
||||||
|
const prescale = scaleLinear().domain([min, max]).nice();
|
||||||
|
let ticks = prescale.ticks(desiredBars);
|
||||||
|
|
||||||
|
const predomain = prescale.domain() as [number, number];
|
||||||
|
|
||||||
|
const minOffset = min - predomain[0];
|
||||||
|
let tickSize = ticks[1] - ticks[0];
|
||||||
|
|
||||||
|
const minBinSize = 1;
|
||||||
|
if (tickSize < minBinSize) {
|
||||||
|
ticks = range(min, max, minBinSize);
|
||||||
|
tickSize = minBinSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minOffset === 0 || (minOffset % tickSize !== 0 && tickSize % minOffset !== 0)) {
|
||||||
|
return [prescale, ticks];
|
||||||
|
}
|
||||||
|
|
||||||
|
const add = (n: number): number => n + minOffset;
|
||||||
|
return [
|
||||||
|
scaleLinear().domain(predomain.map(add) as [number, number]),
|
||||||
|
ticks.map(add),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -8,12 +8,13 @@
|
||||||
import type { GraphsResponse } from "@generated/anki/stats_pb";
|
import type { GraphsResponse } from "@generated/anki/stats_pb";
|
||||||
import * as tr from "@generated/ftl";
|
import * as tr from "@generated/ftl";
|
||||||
import { localizedNumber } from "@tslib/i18n";
|
import { localizedNumber } from "@tslib/i18n";
|
||||||
import type { Bin, ScaleLinear } from "d3";
|
import type { Bin } from "d3";
|
||||||
import { bin, interpolateRdYlGn, scaleLinear, scaleSequential, sum } from "d3";
|
import { bin, interpolateRdYlGn, scaleSequential, sum } from "d3";
|
||||||
|
|
||||||
import type { SearchDispatch, TableDatum } from "./graph-helpers";
|
import type { SearchDispatch, TableDatum } from "./graph-helpers";
|
||||||
import { getNumericMapBinValue, numericMap } from "./graph-helpers";
|
import { getNumericMapBinValue, numericMap } from "./graph-helpers";
|
||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
|
import { getAdjustedScaleAndTicks, percentageRangeMinMax } from "./percentageRange";
|
||||||
|
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
retrievability: Map<number, number>;
|
retrievability: Map<number, number>;
|
||||||
|
|
@ -40,42 +41,18 @@ function makeQuery(start: number, end: number): string {
|
||||||
return `${fromQuery} AND ${tillQuery}`;
|
return `${fromQuery} AND ${tillQuery}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAdjustedScaleAndTicks(
|
|
||||||
min: number,
|
|
||||||
max: number,
|
|
||||||
desiredBars: number,
|
|
||||||
): [ScaleLinear<number, number, never>, number[]] {
|
|
||||||
const prescale = scaleLinear().domain([min, max]).nice();
|
|
||||||
const ticks = prescale.ticks(desiredBars);
|
|
||||||
|
|
||||||
const predomain = prescale.domain() as [number, number];
|
|
||||||
|
|
||||||
const minOffset = min - predomain[0];
|
|
||||||
const tickSize = ticks[1] - ticks[0];
|
|
||||||
|
|
||||||
if (minOffset === 0 || (minOffset % tickSize !== 0 && tickSize % minOffset !== 0)) {
|
|
||||||
return [prescale, ticks];
|
|
||||||
}
|
|
||||||
|
|
||||||
const add = (n: number): number => n + minOffset;
|
|
||||||
return [
|
|
||||||
scaleLinear().domain(predomain.map(add) as [number, number]),
|
|
||||||
ticks.map(add),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prepareData(
|
export function prepareData(
|
||||||
data: GraphData,
|
data: GraphData,
|
||||||
dispatch: SearchDispatch,
|
dispatch: SearchDispatch,
|
||||||
browserLinksSupported: boolean,
|
browserLinksSupported: boolean,
|
||||||
|
quantile?: number,
|
||||||
): [HistogramData | null, TableDatum[]] {
|
): [HistogramData | null, TableDatum[]] {
|
||||||
// get min/max
|
// get min/max
|
||||||
const allEases = data.retrievability;
|
const allEases = data.retrievability;
|
||||||
if (!allEases.size) {
|
if (!allEases.size) {
|
||||||
return [null, []];
|
return [null, []];
|
||||||
}
|
}
|
||||||
const xMin = 0;
|
const [xMin, xMax] = percentageRangeMinMax(allEases, quantile);
|
||||||
const xMax = 100;
|
|
||||||
const desiredBars = 20;
|
const desiredBars = 20;
|
||||||
|
|
||||||
const [scale, ticks] = getAdjustedScaleAndTicks(xMin, xMax, desiredBars);
|
const [scale, ticks] = getAdjustedScaleAndTicks(xMin, xMax, desiredBars);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue