mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
cards added graph
This commit is contained in:
parent
55ec4a2b82
commit
0cab26d40c
11 changed files with 270 additions and 258 deletions
|
@ -988,21 +988,25 @@ message GraphsIn {
|
||||||
}
|
}
|
||||||
|
|
||||||
message GraphsOut {
|
message GraphsOut {
|
||||||
CardsGraphData cards = 1;
|
// CardsGraphData cards = 1;
|
||||||
repeated HourGraphData hours = 2;
|
// TodayGraphData today = 3;
|
||||||
TodayGraphData today = 3;
|
// ButtonsGraphData buttons = 4;
|
||||||
ButtonsGraphData buttons = 4;
|
// repeated HourGraphData hours = 2;
|
||||||
repeated Card cards2 = 5;
|
repeated Card cards = 1;
|
||||||
repeated RevlogEntry revlog = 6;
|
repeated RevlogEntry revlog = 2;
|
||||||
|
|
||||||
|
uint32 days_elapsed = 3;
|
||||||
|
// Based on rollover hour
|
||||||
|
uint32 next_day_at_secs = 4;
|
||||||
|
uint32 scheduler_version = 5;
|
||||||
|
/// Seconds to add to UTC timestamps to get local time.
|
||||||
|
uint32 local_offset_secs = 7;
|
||||||
|
|
||||||
|
uint32 note_count = 10;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message CardsGraphData {
|
message CardsGraphData {
|
||||||
uint32 card_count = 1;
|
|
||||||
uint32 note_count = 2;
|
|
||||||
float ease_factor_min = 3;
|
|
||||||
float ease_factor_max = 4;
|
|
||||||
float ease_factor_sum = 5;
|
|
||||||
uint32 ease_factor_count = 6;
|
|
||||||
uint32 mature_count = 7;
|
uint32 mature_count = 7;
|
||||||
uint32 young_or_learning_count = 8;
|
uint32 young_or_learning_count = 8;
|
||||||
uint32 new_count = 9;
|
uint32 new_count = 9;
|
||||||
|
|
|
@ -94,7 +94,7 @@ impl Collection {
|
||||||
let top_node = Node::Group(parse(search)?);
|
let top_node = Node::Group(parse(search)?);
|
||||||
let writer = SqlWriter::new(self);
|
let writer = SqlWriter::new(self);
|
||||||
|
|
||||||
let (mut sql, args) = writer.build_cards_query(&top_node, RequiredTable::Cards)?;
|
let (sql, args) = writer.build_cards_query(&top_node, RequiredTable::Cards)?;
|
||||||
self.storage.db.execute_batch(concat!(
|
self.storage.db.execute_batch(concat!(
|
||||||
"drop table if exists search_cids;",
|
"drop table if exists search_cids;",
|
||||||
"create temporary table search_cids (id integer primary key not null);"
|
"create temporary table search_cids (id integer primary key not null);"
|
||||||
|
@ -106,7 +106,7 @@ impl Collection {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn clear_searched_cards(&mut self) -> Result<()> {
|
pub(crate) fn clear_searched_cards(&self) -> Result<()> {
|
||||||
self.storage
|
self.storage
|
||||||
.db
|
.db
|
||||||
.execute("drop table if exists search_cids", NO_PARAMS)?;
|
.execute("drop table if exists search_cids", NO_PARAMS)?;
|
||||||
|
|
|
@ -1,220 +1,104 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use crate::{
|
use crate::{backend_proto as pb, prelude::*, revlog::RevlogEntry};
|
||||||
backend_proto as pb,
|
|
||||||
card::{CardQueue, CardType},
|
|
||||||
config::SchedulerVersion,
|
|
||||||
prelude::*,
|
|
||||||
revlog::{RevlogEntry, RevlogReviewKind},
|
|
||||||
sched::cutoff::SchedTimingToday,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct GraphsContext {
|
// impl GraphsContext {
|
||||||
scheduler: SchedulerVersion,
|
// fn observe_button_stats_for_review(&mut self, review: &RevlogEntry) {
|
||||||
timing: SchedTimingToday,
|
// let mut button_num = review.button_chosen as usize;
|
||||||
/// Based on the set rollover hour.
|
// if button_num == 0 {
|
||||||
today_rolled_over_at_millis: i64,
|
// return;
|
||||||
/// Seconds to add to UTC timestamps to get local time.
|
// }
|
||||||
local_offset_secs: i64,
|
|
||||||
stats: AllStats,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
// let buttons = &mut self.stats.buttons;
|
||||||
struct AllStats {
|
// let category = match review.review_kind {
|
||||||
today: pb::TodayGraphData,
|
// RevlogReviewKind::Learning | RevlogReviewKind::Relearning => {
|
||||||
buttons: pb::ButtonsGraphData,
|
// // V1 scheduler only had 3 buttons in learning
|
||||||
hours: Vec<pb::HourGraphData>,
|
// if button_num == 4 && self.scheduler == SchedulerVersion::V1 {
|
||||||
cards: pb::CardsGraphData,
|
// button_num = 3;
|
||||||
cards2: Vec<Card>,
|
// }
|
||||||
revlog: Vec<pb::RevlogEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AllStats {
|
// &mut buttons.learn
|
||||||
fn default() -> Self {
|
// }
|
||||||
let buttons = pb::ButtonsGraphData {
|
// RevlogReviewKind::Review | RevlogReviewKind::EarlyReview => {
|
||||||
learn: vec![0; 4],
|
// if review.last_interval < 21 {
|
||||||
young: vec![0; 4],
|
// &mut buttons.young
|
||||||
mature: vec![0; 4],
|
// } else {
|
||||||
};
|
// &mut buttons.mature
|
||||||
AllStats {
|
// }
|
||||||
today: Default::default(),
|
// }
|
||||||
buttons,
|
// };
|
||||||
hours: vec![Default::default(); 24],
|
|
||||||
cards: Default::default(),
|
|
||||||
cards2: vec![],
|
|
||||||
revlog: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<AllStats> for pb::GraphsOut {
|
// if let Some(count) = category.get_mut(button_num - 1) {
|
||||||
fn from(s: AllStats) -> Self {
|
// *count += 1;
|
||||||
pb::GraphsOut {
|
// }
|
||||||
cards: Some(s.cards),
|
// }
|
||||||
hours: s.hours,
|
|
||||||
today: Some(s.today),
|
|
||||||
buttons: Some(s.buttons),
|
|
||||||
cards2: s.cards2.into_iter().map(Into::into).collect(),
|
|
||||||
revlog: s.revlog,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
// fn observe_hour_stats_for_review(&mut self, review: &RevlogEntry) {
|
||||||
struct ButtonStats {
|
// match review.review_kind {
|
||||||
/// In V1 scheduler, 4th element is ignored
|
// RevlogReviewKind::Learning
|
||||||
learn: [u32; 4],
|
// | RevlogReviewKind::Review
|
||||||
young: [u32; 4],
|
// | RevlogReviewKind::Relearning => {
|
||||||
mature: [u32; 4],
|
// let hour_idx = (((review.id.0 / 1000) + self.local_offset_secs) / 3600) % 24;
|
||||||
}
|
// let hour = &mut self.stats.hours[hour_idx as usize];
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
// hour.review_count += 1;
|
||||||
struct HourStats {
|
// if review.button_chosen != 1 {
|
||||||
review_count: u32,
|
// hour.correct_count += 1;
|
||||||
correct_count: u32,
|
// }
|
||||||
}
|
// }
|
||||||
|
// RevlogReviewKind::EarlyReview => {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
// fn observe_today_stats_for_review(&mut self, review: &RevlogEntry) {
|
||||||
struct CardStats {
|
// if review.id.0 < self.today_rolled_over_at_millis {
|
||||||
card_count: u32,
|
// return;
|
||||||
note_count: u32,
|
// }
|
||||||
ease_factor_min: f32,
|
|
||||||
ease_factor_max: f32,
|
|
||||||
ease_factor_sum: f32,
|
|
||||||
ease_factor_count: u32,
|
|
||||||
mature_count: u32,
|
|
||||||
young_or_learning_count: u32,
|
|
||||||
new_count: u32,
|
|
||||||
suspended_or_buried_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphsContext {
|
// let today = &mut self.stats.today;
|
||||||
fn observe_card(&mut self, card: &Card) {
|
|
||||||
self.observe_card_stats_for_card(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn observe_review(&mut self, entry: &RevlogEntry) {
|
// // total
|
||||||
self.observe_button_stats_for_review(entry);
|
// today.answer_count += 1;
|
||||||
self.observe_hour_stats_for_review(entry);
|
// today.answer_millis += review.taken_millis;
|
||||||
self.observe_today_stats_for_review(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn observe_button_stats_for_review(&mut self, review: &RevlogEntry) {
|
// // correct
|
||||||
let mut button_num = review.button_chosen as usize;
|
// if review.button_chosen > 1 {
|
||||||
if button_num == 0 {
|
// today.correct_count += 1;
|
||||||
return;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
let buttons = &mut self.stats.buttons;
|
// // mature
|
||||||
let category = match review.review_kind {
|
// if review.last_interval >= 21 {
|
||||||
RevlogReviewKind::Learning | RevlogReviewKind::Relearning => {
|
// today.mature_count += 1;
|
||||||
// V1 scheduler only had 3 buttons in learning
|
// if review.button_chosen > 1 {
|
||||||
if button_num == 4 && self.scheduler == SchedulerVersion::V1 {
|
// today.mature_correct += 1;
|
||||||
button_num = 3;
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
&mut buttons.learn
|
// // type counts
|
||||||
}
|
// match review.review_kind {
|
||||||
RevlogReviewKind::Review | RevlogReviewKind::EarlyReview => {
|
// RevlogReviewKind::Learning => today.learn_count += 1,
|
||||||
if review.last_interval < 21 {
|
// RevlogReviewKind::Review => today.review_count += 1,
|
||||||
&mut buttons.young
|
// RevlogReviewKind::Relearning => today.relearn_count += 1,
|
||||||
} else {
|
// RevlogReviewKind::EarlyReview => today.early_review_count += 1,
|
||||||
&mut buttons.mature
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(count) = category.get_mut(button_num - 1) {
|
// fn observe_card_stats_for_card(&mut self, card: &Card) {
|
||||||
*count += 1;
|
// counts by type
|
||||||
}
|
// match card.queue {
|
||||||
}
|
// CardQueue::New => cstats.new_count += 1,
|
||||||
|
// CardQueue::Review if card.ivl >= 21 => cstats.mature_count += 1,
|
||||||
fn observe_hour_stats_for_review(&mut self, review: &RevlogEntry) {
|
// CardQueue::Review | CardQueue::Learn | CardQueue::DayLearn => {
|
||||||
match review.review_kind {
|
// cstats.young_or_learning_count += 1
|
||||||
RevlogReviewKind::Learning
|
// }
|
||||||
| RevlogReviewKind::Review
|
// CardQueue::Suspended | CardQueue::UserBuried | CardQueue::SchedBuried => {
|
||||||
| RevlogReviewKind::Relearning => {
|
// cstats.suspended_or_buried_count += 1
|
||||||
let hour_idx = (((review.id.0 / 1000) + self.local_offset_secs) / 3600) % 24;
|
// }
|
||||||
let hour = &mut self.stats.hours[hour_idx as usize];
|
// CardQueue::PreviewRepeat => {}
|
||||||
|
// }
|
||||||
hour.review_count += 1;
|
// }
|
||||||
if review.button_chosen != 1 {
|
// }
|
||||||
hour.correct_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RevlogReviewKind::EarlyReview => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn observe_today_stats_for_review(&mut self, review: &RevlogEntry) {
|
|
||||||
if review.id.0 < self.today_rolled_over_at_millis {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let today = &mut self.stats.today;
|
|
||||||
|
|
||||||
// total
|
|
||||||
today.answer_count += 1;
|
|
||||||
today.answer_millis += review.taken_millis;
|
|
||||||
|
|
||||||
// correct
|
|
||||||
if review.button_chosen > 1 {
|
|
||||||
today.correct_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// mature
|
|
||||||
if review.last_interval >= 21 {
|
|
||||||
today.mature_count += 1;
|
|
||||||
if review.button_chosen > 1 {
|
|
||||||
today.mature_correct += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// type counts
|
|
||||||
match review.review_kind {
|
|
||||||
RevlogReviewKind::Learning => today.learn_count += 1,
|
|
||||||
RevlogReviewKind::Review => today.review_count += 1,
|
|
||||||
RevlogReviewKind::Relearning => today.relearn_count += 1,
|
|
||||||
RevlogReviewKind::EarlyReview => today.early_review_count += 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn observe_card_stats_for_card(&mut self, card: &Card) {
|
|
||||||
let cstats = &mut self.stats.cards;
|
|
||||||
|
|
||||||
cstats.card_count += 1;
|
|
||||||
|
|
||||||
// counts by type
|
|
||||||
match card.queue {
|
|
||||||
CardQueue::New => cstats.new_count += 1,
|
|
||||||
CardQueue::Review if card.ivl >= 21 => cstats.mature_count += 1,
|
|
||||||
CardQueue::Review | CardQueue::Learn | CardQueue::DayLearn => {
|
|
||||||
cstats.young_or_learning_count += 1
|
|
||||||
}
|
|
||||||
CardQueue::Suspended | CardQueue::UserBuried | CardQueue::SchedBuried => {
|
|
||||||
cstats.suspended_or_buried_count += 1
|
|
||||||
}
|
|
||||||
CardQueue::PreviewRepeat => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ease factor
|
|
||||||
if card.ctype == CardType::Review {
|
|
||||||
let ease_factor = (card.factor as f32) / 1000.0;
|
|
||||||
|
|
||||||
cstats.ease_factor_count += 1;
|
|
||||||
cstats.ease_factor_sum += ease_factor;
|
|
||||||
|
|
||||||
if ease_factor < cstats.ease_factor_min || cstats.ease_factor_min == 0.0 {
|
|
||||||
cstats.ease_factor_min = ease_factor;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ease_factor > cstats.ease_factor_max {
|
|
||||||
cstats.ease_factor_max = ease_factor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub(crate) fn graph_data_for_search(
|
pub(crate) fn graph_data_for_search(
|
||||||
|
@ -223,14 +107,11 @@ impl Collection {
|
||||||
days: u32,
|
days: u32,
|
||||||
) -> Result<pb::GraphsOut> {
|
) -> Result<pb::GraphsOut> {
|
||||||
self.search_cards_into_table(search)?;
|
self.search_cards_into_table(search)?;
|
||||||
let i = std::time::Instant::now();
|
|
||||||
let all = search.trim().is_empty();
|
let all = search.trim().is_empty();
|
||||||
let stats = self.graph_data(all, days)?;
|
self.graph_data(all, days)
|
||||||
let stats = stats.into();
|
|
||||||
Ok(stats)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn graph_data(&self, all: bool, days: u32) -> Result<AllStats> {
|
fn graph_data(&self, all: bool, days: u32) -> Result<pb::GraphsOut> {
|
||||||
let timing = self.timing_today()?;
|
let timing = self.timing_today()?;
|
||||||
let revlog_start = TimestampSecs(if days > 0 {
|
let revlog_start = TimestampSecs(if days > 0 {
|
||||||
timing.next_day_at - (((days as i64) + 1) * 86_400)
|
timing.next_day_at - (((days as i64) + 1) * 86_400)
|
||||||
|
@ -241,14 +122,6 @@ impl Collection {
|
||||||
let offset = self.local_offset();
|
let offset = self.local_offset();
|
||||||
let local_offset_secs = offset.local_minus_utc() as i64;
|
let local_offset_secs = offset.local_minus_utc() as i64;
|
||||||
|
|
||||||
let mut ctx = GraphsContext {
|
|
||||||
scheduler: self.sched_ver(),
|
|
||||||
today_rolled_over_at_millis: (timing.next_day_at - 86_400) * 1000,
|
|
||||||
timing,
|
|
||||||
local_offset_secs,
|
|
||||||
stats: AllStats::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let cards = self.storage.all_searched_cards()?;
|
let cards = self.storage.all_searched_cards()?;
|
||||||
let revlog = if all {
|
let revlog = if all {
|
||||||
self.storage.get_all_revlog_entries(revlog_start)?
|
self.storage.get_all_revlog_entries(revlog_start)?
|
||||||
|
@ -257,12 +130,17 @@ impl Collection {
|
||||||
.get_revlog_entries_for_searched_cards(revlog_start)?
|
.get_revlog_entries_for_searched_cards(revlog_start)?
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.stats.cards2 = cards;
|
self.clear_searched_cards()?;
|
||||||
ctx.stats.revlog = revlog;
|
|
||||||
|
|
||||||
// ctx.stats.cards.note_count = self.storage.note_ids_of_cards(cids)?.len() as u32;
|
Ok(pb::GraphsOut {
|
||||||
|
cards: cards.into_iter().map(Into::into).collect(),
|
||||||
Ok(ctx.stats)
|
revlog,
|
||||||
|
days_elapsed: timing.days_elapsed,
|
||||||
|
next_day_at_secs: timing.next_day_at as u32,
|
||||||
|
scheduler_version: self.sched_ver() as u32,
|
||||||
|
local_offset_secs: local_offset_secs as u32,
|
||||||
|
note_count: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -251,23 +251,6 @@ impl super::SqliteStorage {
|
||||||
Ok(nids)
|
Ok(nids)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn for_each_card_in_search<F>(&self, mut func: F) -> Result<()>
|
|
||||||
where
|
|
||||||
F: FnMut(&Card) -> Result<()>,
|
|
||||||
{
|
|
||||||
let mut stmt = self.db.prepare_cached(concat!(
|
|
||||||
include_str!("get_card.sql"),
|
|
||||||
" where id in (select id from search_cids)"
|
|
||||||
))?;
|
|
||||||
let mut rows = stmt.query(NO_PARAMS)?;
|
|
||||||
while let Some(row) = rows.next()? {
|
|
||||||
let entry = row_to_card(row)?;
|
|
||||||
func(&entry)?
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn all_searched_cards(&self) -> Result<Vec<Card>> {
|
pub(crate) fn all_searched_cards(&self) -> Result<Vec<Card>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached(concat!(
|
.prepare_cached(concat!(
|
||||||
|
|
|
@ -9,6 +9,10 @@ module.exports = {
|
||||||
rules: {
|
rules: {
|
||||||
"prefer-const": "warn",
|
"prefer-const": "warn",
|
||||||
"@typescript-eslint/ban-ts-ignore": "warn",
|
"@typescript-eslint/ban-ts-ignore": "warn",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
|
|
50
ts/src/stats/AddedGraph.svelte
Normal file
50
ts/src/stats/AddedGraph.svelte
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="typescript">
|
||||||
|
import { HistogramData } from "./histogram-graph";
|
||||||
|
import { gatherData, prepareData, GraphData, AddedRange } from "./added";
|
||||||
|
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;
|
||||||
|
let range = AddedRange.Month;
|
||||||
|
|
||||||
|
let addedData: GraphData | null = null;
|
||||||
|
$: if (data) {
|
||||||
|
console.log("gathering data");
|
||||||
|
addedData = gatherData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (addedData) {
|
||||||
|
console.log("preparing data");
|
||||||
|
histogramData = prepareData(addedData, range);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if histogramData}
|
||||||
|
<div class="graph">
|
||||||
|
<h1>Added</h1>
|
||||||
|
|
||||||
|
<div class="range-box">
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={AddedRange.Month} />
|
||||||
|
Month
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={AddedRange.Quarter} />
|
||||||
|
3 months
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={AddedRange.Year} />
|
||||||
|
Year
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={AddedRange.AllTime} />
|
||||||
|
All time
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HistogramGraph data={histogramData} xText="Days" yText="Number of cards" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -10,6 +10,7 @@
|
||||||
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";
|
import EaseGraph from "./EaseGraph.svelte";
|
||||||
|
import AddedGraph from "./AddedGraph.svelte";
|
||||||
|
|
||||||
let data: pb.BackendProto.GraphsOut | null = null;
|
let data: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
|
||||||
|
@ -107,5 +108,6 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AddedGraph {data} />
|
||||||
<IntervalsGraph {data} />
|
<IntervalsGraph {data} />
|
||||||
<EaseGraph {data} />
|
<EaseGraph {data} />
|
||||||
|
|
82
ts/src/stats/added.ts
Normal file
82
ts/src/stats/added.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// 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 { HistogramData } from "./histogram-graph";
|
||||||
|
import { interpolateBlues } from "d3-scale-chromatic";
|
||||||
|
|
||||||
|
export enum AddedRange {
|
||||||
|
Month = 0,
|
||||||
|
Quarter = 1,
|
||||||
|
Year = 2,
|
||||||
|
AllTime = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphData {
|
||||||
|
daysAdded: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
||||||
|
const daysAdded = (data.cards as pb.BackendProto.Card[]).map((card) => {
|
||||||
|
const elapsedSecs = (card.id as number) / 1000 - data.nextDayAtSecs;
|
||||||
|
return Math.ceil(elapsedSecs / 86400);
|
||||||
|
});
|
||||||
|
return { daysAdded };
|
||||||
|
}
|
||||||
|
|
||||||
|
function hoverText(
|
||||||
|
data: HistogramData,
|
||||||
|
binIdx: number,
|
||||||
|
cumulative: number,
|
||||||
|
_percent: number
|
||||||
|
): string {
|
||||||
|
const bin = data.bins[binIdx];
|
||||||
|
return (
|
||||||
|
`${bin.length} at ${bin.x1! - 1} days.<br>` +
|
||||||
|
` ${cumulative} cards at or below this point.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareData(data: GraphData, range: AddedRange): HistogramData | null {
|
||||||
|
// get min/max
|
||||||
|
const total = data.daysAdded.length;
|
||||||
|
if (!total) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [xMinOrig, _xMax] = extent(data.daysAdded);
|
||||||
|
let xMin = xMinOrig;
|
||||||
|
|
||||||
|
// cap max to selected range
|
||||||
|
switch (range) {
|
||||||
|
case AddedRange.Month:
|
||||||
|
xMin = -31;
|
||||||
|
break;
|
||||||
|
case AddedRange.Quarter:
|
||||||
|
xMin = -90;
|
||||||
|
break;
|
||||||
|
case AddedRange.Year:
|
||||||
|
xMin = -365;
|
||||||
|
break;
|
||||||
|
case AddedRange.AllTime:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const xMax = 1;
|
||||||
|
const desiredBars = Math.min(70, Math.abs(xMin!));
|
||||||
|
|
||||||
|
const scale = scaleLinear().domain([xMin!, xMax]);
|
||||||
|
const bins = histogram()
|
||||||
|
.domain(scale.domain() as any)
|
||||||
|
.thresholds(scale.ticks(desiredBars))(data.daysAdded);
|
||||||
|
|
||||||
|
const colourScale = scaleSequential(interpolateBlues).domain([xMin!, xMax]);
|
||||||
|
|
||||||
|
return { scale, bins, total, hoverText, colourScale, showArea: true };
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ export interface GraphData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
||||||
const eases = (data.cards2 as pb.BackendProto.Card[])
|
const eases = (data.cards as pb.BackendProto.Card[])
|
||||||
.filter((c) => c.queue == CardQueue.Review)
|
.filter((c) => c.queue == CardQueue.Review)
|
||||||
.map((c) => c.factor / 10);
|
.map((c) => c.factor / 10);
|
||||||
return { eases };
|
return { eases };
|
||||||
|
|
|
@ -9,8 +9,7 @@
|
||||||
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, interpolateRdYlGn } from "d3-scale-chromatic";
|
import { scaleLinear, ScaleLinear, ScaleSequential } 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";
|
||||||
|
@ -20,7 +19,12 @@ export interface HistogramData {
|
||||||
scale: ScaleLinear<number, number>;
|
scale: ScaleLinear<number, number>;
|
||||||
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,
|
||||||
|
cumulative: number,
|
||||||
|
percent: number
|
||||||
|
) => string;
|
||||||
showArea: boolean;
|
showArea: boolean;
|
||||||
colourScale: ScaleSequential<string>;
|
colourScale: ScaleSequential<string>;
|
||||||
}
|
}
|
||||||
|
@ -66,7 +70,7 @@ export function histogramGraph(
|
||||||
.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) => data.colourScale(d.x1));
|
.attr("fill", (d) => data.colourScale(d.x1));
|
||||||
};
|
};
|
||||||
|
|
||||||
svg.select("g.bars")
|
svg.select("g.bars")
|
||||||
|
@ -125,7 +129,7 @@ export function histogramGraph(
|
||||||
.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 = data.showArea ? (areaData[idx + 1] / data.total) * 100 : 0;
|
const pct = data.showArea ? (areaData[idx + 1] / data.total) * 100 : 0;
|
||||||
showTooltip(data.hoverText(data, idx, pct), x, y);
|
showTooltip(data.hoverText(data, idx, areaData[idx + 1], pct), x, y);
|
||||||
})
|
})
|
||||||
.on("mouseout", hideTooltip);
|
.on("mouseout", hideTooltip);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,13 +26,18 @@ export enum IntervalRange {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gatherIntervalData(data: pb.BackendProto.GraphsOut): IntervalGraphData {
|
export function gatherIntervalData(data: pb.BackendProto.GraphsOut): IntervalGraphData {
|
||||||
const intervals = (data.cards2 as pb.BackendProto.Card[])
|
const intervals = (data.cards as pb.BackendProto.Card[])
|
||||||
.filter((c) => c.queue == CardQueue.Review)
|
.filter((c) => c.queue == CardQueue.Review)
|
||||||
.map((c) => c.ivl);
|
.map((c) => c.ivl);
|
||||||
return { intervals };
|
return { intervals };
|
||||||
}
|
}
|
||||||
|
|
||||||
function hoverText(data: HistogramData, binIdx: number, percent: number): string {
|
function hoverText(
|
||||||
|
data: HistogramData,
|
||||||
|
binIdx: number,
|
||||||
|
_cumulative: number,
|
||||||
|
percent: number
|
||||||
|
): string {
|
||||||
const bin = data.bins[binIdx];
|
const bin = data.bins[binIdx];
|
||||||
const interval =
|
const interval =
|
||||||
bin.x1! - bin.x0! === 1
|
bin.x1! - bin.x0! === 1
|
||||||
|
|
Loading…
Reference in a new issue