From 0abac06c39d900ec32ad961f51aaa87cff4fc60a Mon Sep 17 00:00:00 2001 From: junlu592 Date: Sat, 15 Nov 2025 12:51:21 +0100 Subject: [PATCH 01/32] Fixed overlapping ranges when all history is selected and days calcolation to prevent overlapping labels --- CONTRIBUTORS | 1 + ts/lib/tslib/time.ts | 6 ++++-- ts/routes/graphs/calendar.ts | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 2ec25da2d..0ef0d1eb8 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -255,6 +255,7 @@ Ranjit Odedra Eltaurus jariji Francisco Esteva +Junia Mannervik ******************** diff --git a/ts/lib/tslib/time.ts b/ts/lib/tslib/time.ts index 25d70eef3..0e6330f38 100644 --- a/ts/lib/tslib/time.ts +++ b/ts/lib/tslib/time.ts @@ -178,9 +178,11 @@ export function dayLabel(daysStart: number, daysEnd: number): string { daysEnd: daysEnd - 1, }); } else { + const mostRecent = daysEnd <= 0 ? Math.abs(daysEnd - 1) : 0; + const oldest = -daysStart; return tr.statisticsDaysAgoRange({ - daysStart: Math.abs(daysEnd - 1), - daysEnd: -daysStart, + daysStart: mostRecent, + daysEnd: oldest, }); } } diff --git a/ts/routes/graphs/calendar.ts b/ts/routes/graphs/calendar.ts index 863923095..14c32d089 100644 --- a/ts/routes/graphs/calendar.ts +++ b/ts/routes/graphs/calendar.ts @@ -118,6 +118,10 @@ export function renderCalendar( // don't fill out future dates continue; } + if (date.getFullYear() != targetYear) { + // only fill blanks for the target year + continue; + } if (revlogRange == RevlogRange.Year && date < oneYearAgoFromNow) { // don't fill out dates older than a year continue; From e4c6e082f09457f6b14e1a1149f21353b2146cc3 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Thu, 20 Nov 2025 13:20:40 +0100 Subject: [PATCH 02/32] fix statistics calendar and review grapgh range labeling --- ts/routes/graphs/reviews.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index ed0e07f8c..8302cb4c9 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -111,9 +111,6 @@ export function renderReviews( const desiredBars = Math.min(70, Math.abs(xMin!)); const x = scaleLinear().domain([xMin!, xMax]); - if (range === GraphRange.AllTime) { - x.nice(desiredBars); - } const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; const bins = bin() @@ -212,7 +209,9 @@ export function renderReviews( } function tooltipText(d: BinType, cumulative: number): string { - const day = dayLabel(d.x0!, d.x1!); + const startDay = Math.trunc(d.x0!); + const endDay = Math.trunc(d.x1!); + const day = dayLabel(startDay, endDay); const totals = totalsForBin(d); const dayTotal = valueLabel(sum(totals)); let buf = ``; From e013ee61b5adfbccd6d8564ad7fc04eaa7dc75e5 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Thu, 20 Nov 2025 13:42:53 +0100 Subject: [PATCH 03/32] update graph so that the tooltip day label uses integer day boundaries --- CONTRIBUTORS | 1 + ts/lib/tslib/time.ts | 6 ++++-- ts/routes/graphs/calendar.ts | 4 ++++ ts/routes/graphs/reviews.ts | 8 ++++---- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index d90b7dbcc..d19044034 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -254,6 +254,7 @@ nav1s Ranjit Odedra Eltaurus jariji +Junia Mannervik ******************** diff --git a/ts/lib/tslib/time.ts b/ts/lib/tslib/time.ts index 25d70eef3..0e6330f38 100644 --- a/ts/lib/tslib/time.ts +++ b/ts/lib/tslib/time.ts @@ -178,9 +178,11 @@ export function dayLabel(daysStart: number, daysEnd: number): string { daysEnd: daysEnd - 1, }); } else { + const mostRecent = daysEnd <= 0 ? Math.abs(daysEnd - 1) : 0; + const oldest = -daysStart; return tr.statisticsDaysAgoRange({ - daysStart: Math.abs(daysEnd - 1), - daysEnd: -daysStart, + daysStart: mostRecent, + daysEnd: oldest, }); } } diff --git a/ts/routes/graphs/calendar.ts b/ts/routes/graphs/calendar.ts index 863923095..14c32d089 100644 --- a/ts/routes/graphs/calendar.ts +++ b/ts/routes/graphs/calendar.ts @@ -118,6 +118,10 @@ export function renderCalendar( // don't fill out future dates continue; } + if (date.getFullYear() != targetYear) { + // only fill blanks for the target year + continue; + } if (revlogRange == RevlogRange.Year && date < oneYearAgoFromNow) { // don't fill out dates older than a year continue; diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index ed0e07f8c..35db5868a 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -111,9 +111,7 @@ export function renderReviews( const desiredBars = Math.min(70, Math.abs(xMin!)); const x = scaleLinear().domain([xMin!, xMax]); - if (range === GraphRange.AllTime) { - x.nice(desiredBars); - } + const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; const bins = bin() @@ -212,7 +210,9 @@ export function renderReviews( } function tooltipText(d: BinType, cumulative: number): string { - const day = dayLabel(d.x0!, d.x1!); + const startDay = Math.trunc(d.x0!); + const endDay = Math.trunc(d.x1!); + const day = dayLabel(startDay, endDay); const totals = totalsForBin(d); const dayTotal = valueLabel(sum(totals)); let buf = `
${day}${dayTotal}
`; From 9ccce956243fbef41ca380efc0e8c48b9a80a04d Mon Sep 17 00:00:00 2001 From: llama Date: Thu, 20 Nov 2025 03:48:14 +0800 Subject: [PATCH 04/32] fix(import): support variable field count with notetype column (#4421) --- rslib/src/import_export/text/csv/import.rs | 22 +++++++++++++++----- rslib/src/import_export/text/csv/metadata.rs | 7 ++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/rslib/src/import_export/text/csv/import.rs b/rslib/src/import_export/text/csv/import.rs index e45bbca1b..31dee84e4 100644 --- a/rslib/src/import_export/text/csv/import.rs +++ b/rslib/src/import_export/text/csv/import.rs @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use std::collections::HashSet; use std::io::BufRead; use std::io::BufReader; use std::io::Read; @@ -106,6 +107,8 @@ struct ColumnContext { notetype_column: Option, /// Source column indices for the fields of a notetype field_source_columns: FieldSourceColumns, + /// Metadata column indices (1-based) + meta_columns: HashSet, /// How fields are converted to strings. Used for escaping HTML if /// appropriate. stringify: fn(&str) -> String, @@ -119,6 +122,7 @@ impl ColumnContext { deck_column: metadata.deck()?.column(), notetype_column: metadata.notetype()?.column(), field_source_columns: metadata.field_source_columns()?, + meta_columns: metadata.meta_columns(), stringify: stringify_fn(metadata.is_html), }) } @@ -166,11 +170,19 @@ impl ColumnContext { } fn gather_note_fields(&self, record: &csv::StringRecord) -> Vec> { - let stringify = self.stringify; - self.field_source_columns - .iter() - .map(|opt| opt.and_then(|idx| record.get(idx - 1)).map(stringify)) - .collect() + let op = |i| record.get(i - 1).map(self.stringify); + if !self.field_source_columns.is_empty() { + self.field_source_columns + .iter() + .map(|opt| opt.and_then(op)) + .collect() + } else { + // notetype column provided, assume all non-metadata columns are notetype fields + (1..=record.len()) + .filter(|i| !self.meta_columns.contains(i)) + .map(op) + .collect() + } } } diff --git a/rslib/src/import_export/text/csv/metadata.rs b/rslib/src/import_export/text/csv/metadata.rs index d505c60d2..cd4150813 100644 --- a/rslib/src/import_export/text/csv/metadata.rs +++ b/rslib/src/import_export/text/csv/metadata.rs @@ -291,11 +291,8 @@ impl CsvMetadataHelpers for CsvMetadata { .map(|&i| (i > 0).then_some(i as usize)) .collect(), CsvNotetype::NotetypeColumn(_) => { - let meta_columns = self.meta_columns(); - (1..self.column_labels.len() + 1) - .filter(|idx| !meta_columns.contains(idx)) - .map(Some) - .collect() + // each row's notetype could have varying number of fields + vec![] } }) } From 26e5ae2f672ef58e585724ec160dad4a6e230e7b Mon Sep 17 00:00:00 2001 From: junlu592 Date: Thu, 20 Nov 2025 13:42:53 +0100 Subject: [PATCH 05/32] update graph so that the tooltip day label uses integer day boundaries and removed earlier fix which didnt work --- CONTRIBUTORS | 1 + ts/routes/graphs/reviews.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 2ec25da2d..0ef0d1eb8 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -255,6 +255,7 @@ Ranjit Odedra Eltaurus jariji Francisco Esteva +Junia Mannervik ******************** diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index ed0e07f8c..35db5868a 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -111,9 +111,7 @@ export function renderReviews( const desiredBars = Math.min(70, Math.abs(xMin!)); const x = scaleLinear().domain([xMin!, xMax]); - if (range === GraphRange.AllTime) { - x.nice(desiredBars); - } + const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; const bins = bin() @@ -212,7 +210,9 @@ export function renderReviews( } function tooltipText(d: BinType, cumulative: number): string { - const day = dayLabel(d.x0!, d.x1!); + const startDay = Math.trunc(d.x0!); + const endDay = Math.trunc(d.x1!); + const day = dayLabel(startDay, endDay); const totals = totalsForBin(d); const dayTotal = valueLabel(sum(totals)); let buf = `
${day}${dayTotal}
`; From a71203c6f190e8b9c567663d28100945750a6a24 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Sat, 15 Nov 2025 12:51:21 +0100 Subject: [PATCH 06/32] Fixed overlapping ranges when all history is selected and days calcolation to prevent overlapping labels --- ts/lib/tslib/time.ts | 6 ++++-- ts/routes/graphs/calendar.ts | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ts/lib/tslib/time.ts b/ts/lib/tslib/time.ts index 25d70eef3..0e6330f38 100644 --- a/ts/lib/tslib/time.ts +++ b/ts/lib/tslib/time.ts @@ -178,9 +178,11 @@ export function dayLabel(daysStart: number, daysEnd: number): string { daysEnd: daysEnd - 1, }); } else { + const mostRecent = daysEnd <= 0 ? Math.abs(daysEnd - 1) : 0; + const oldest = -daysStart; return tr.statisticsDaysAgoRange({ - daysStart: Math.abs(daysEnd - 1), - daysEnd: -daysStart, + daysStart: mostRecent, + daysEnd: oldest, }); } } diff --git a/ts/routes/graphs/calendar.ts b/ts/routes/graphs/calendar.ts index 863923095..14c32d089 100644 --- a/ts/routes/graphs/calendar.ts +++ b/ts/routes/graphs/calendar.ts @@ -118,6 +118,10 @@ export function renderCalendar( // don't fill out future dates continue; } + if (date.getFullYear() != targetYear) { + // only fill blanks for the target year + continue; + } if (revlogRange == RevlogRange.Year && date < oneYearAgoFromNow) { // don't fill out dates older than a year continue; From d0cffd915de9a6dc243353a01d0c821cc81c73c9 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Thu, 20 Nov 2025 13:20:40 +0100 Subject: [PATCH 07/32] fix statistics calendar and review graph range labeling and removed earlier changes --- ts/routes/graphs/reviews.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 35db5868a..8302cb4c9 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -112,7 +112,6 @@ export function renderReviews( const x = scaleLinear().domain([xMin!, xMax]); - const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; const bins = bin() .value((m) => { From e6ed5f6d8da6aa875ce86e63d33f040cac3eecc9 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Fri, 21 Nov 2025 12:17:37 +0100 Subject: [PATCH 08/32] reverted changes in time.ts --- ts/lib/tslib/time.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ts/lib/tslib/time.ts b/ts/lib/tslib/time.ts index 0e6330f38..25d70eef3 100644 --- a/ts/lib/tslib/time.ts +++ b/ts/lib/tslib/time.ts @@ -178,11 +178,9 @@ export function dayLabel(daysStart: number, daysEnd: number): string { daysEnd: daysEnd - 1, }); } else { - const mostRecent = daysEnd <= 0 ? Math.abs(daysEnd - 1) : 0; - const oldest = -daysStart; return tr.statisticsDaysAgoRange({ - daysStart: mostRecent, - daysEnd: oldest, + daysStart: Math.abs(daysEnd - 1), + daysEnd: -daysStart, }); } } From 7692ca65801b50fa2bb579e967383e69f1210527 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Fri, 21 Nov 2025 12:18:43 +0100 Subject: [PATCH 09/32] reverted changes in calendar.ts --- ts/routes/graphs/calendar.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ts/routes/graphs/calendar.ts b/ts/routes/graphs/calendar.ts index 14c32d089..863923095 100644 --- a/ts/routes/graphs/calendar.ts +++ b/ts/routes/graphs/calendar.ts @@ -118,10 +118,6 @@ export function renderCalendar( // don't fill out future dates continue; } - if (date.getFullYear() != targetYear) { - // only fill blanks for the target year - continue; - } if (revlogRange == RevlogRange.Year && date < oneYearAgoFromNow) { // don't fill out dates older than a year continue; From a3fc69fe898f8d2019d0a16d5485fe7f6e7db115 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Fri, 21 Nov 2025 12:52:51 +0100 Subject: [PATCH 10/32] oldest bin width modified to match other bins in reviews.ts --- ts/routes/graphs/reviews.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 8302cb4c9..4f23698b7 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -110,7 +110,17 @@ export function renderReviews( } const desiredBars = Math.min(70, Math.abs(xMin!)); - const x = scaleLinear().domain([xMin!, xMax]); + let x = scaleLinear().domain([xMin!, xMax]); + let thresholds = x.ticks(desiredBars); + if (thresholds.length >= 2) { + const spacing = thresholds[1] - thresholds[0]; + const partial = thresholds[0] - x.domain()[0]; + if (spacing > 0 && partial > 0 && partial < spacing) { + const adjustedMin = thresholds[0] - spacing; + x = scaleLinear().domain([adjustedMin, xMax]); + thresholds = x.ticks(desiredBars); + } + } const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; const bins = bin() @@ -118,7 +128,7 @@ export function renderReviews( return m[0]; }) .domain(x.domain() as any) - .thresholds(x.ticks(desiredBars))(sourceMap.entries() as any); + .thresholds(thresholds)(sourceMap.entries() as any); // empty graph? const totalDays = sum(bins, (bin) => bin.length); From e3546ea496f5d5dfd99aecd309212ca71d848b23 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Fri, 21 Nov 2025 15:32:16 +0100 Subject: [PATCH 11/32] modified youngest bar in reviews to include today aswell --- ts/routes/graphs/reviews.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 4f23698b7..35deeb81c 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -123,13 +123,27 @@ export function renderReviews( } const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; - const bins = bin() + let bins = bin() .value((m) => { return m[0]; }) .domain(x.domain() as any) .thresholds(thresholds)(sourceMap.entries() as any); + if (bins.length > 1 && thresholds.length > 1) { + const lastBin = bins[bins.length - 1]; + const prevBin = bins[bins.length - 2]; + const nominalWidth = thresholds[1] - thresholds[0]; + const lastWidth = lastBin.x1! - lastBin.x0!; + if (lastBin.x1! > 0 && lastWidth < nominalWidth * 0.75) { + for (const entry of lastBin) { + prevBin.push(entry); + } + prevBin.x1 = lastBin.x1; + bins = bins.slice(0, -1); + } + } + // empty graph? const totalDays = sum(bins, (bin) => bin.length); if (!totalDays) { @@ -219,8 +233,8 @@ export function renderReviews( } function tooltipText(d: BinType, cumulative: number): string { - const startDay = Math.trunc(d.x0!); - const endDay = Math.trunc(d.x1!); + const startDay = Math.floor(d.x0!); + const endDay = Math.ceil(d.x1!); const day = dayLabel(startDay, endDay); const totals = totalsForBin(d); const dayTotal = valueLabel(sum(totals)); From 2db3aed716b601a60f7d182d40369cfd6d749b29 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Mon, 1 Dec 2025 12:55:16 +0100 Subject: [PATCH 12/32] Fix reviews graph: correct Year range to 365 days --- ts/routes/graphs/reviews.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 35deeb81c..7255c5ea2 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -91,18 +91,21 @@ export function renderReviews( const svg = select(svgElem); const trans = svg.transition().duration(600) as any; - const xMax = 1; + let xMax = 1; let xMin = 0; // cap max to selected range switch (range) { case GraphRange.Month: xMin = -30; + xMax = 0; break; case GraphRange.ThreeMonths: xMin = -89; + xMax = 0; break; case GraphRange.Year: xMin = -364; + xMax = 0; break; case GraphRange.AllTime: xMin = min(sourceData.reviewCount.keys())!; From 2da59a4768181c0d2ccf7a05cf0c92c824debe7c Mon Sep 17 00:00:00 2001 From: junlu592 Date: Mon, 1 Dec 2025 12:59:10 +0100 Subject: [PATCH 13/32] fixed formating for ninja testing --- rslib/src/revlog/mod.rs | 2 +- ts/routes/graphs/reviews.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rslib/src/revlog/mod.rs b/rslib/src/revlog/mod.rs index fbb9b459a..3e22890b1 100644 --- a/rslib/src/revlog/mod.rs +++ b/rslib/src/revlog/mod.rs @@ -161,7 +161,7 @@ impl Collection { ) -> Result<()> { let ease_factor = u32::from( card.memory_state - .map(|s| ((s.difficulty_shifted() * 1000.) as u16)) + .map(|s| (s.difficulty_shifted() * 1000.) as u16) .unwrap_or(card.ease_factor), ); let entry = RevlogEntry { diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 7255c5ea2..5235939fc 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -105,7 +105,7 @@ export function renderReviews( break; case GraphRange.Year: xMin = -364; - xMax = 0; + xMax = 0; break; case GraphRange.AllTime: xMin = min(sourceData.reviewCount.keys())!; From 8a8f07c3c303fe4651affca6379293c6d13f7d94 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Mon, 1 Dec 2025 13:16:47 +0100 Subject: [PATCH 14/32] bin labels and totals correct for year, review --- ts/lib/tslib/time.ts | 41 ++++++++++++++++++++++++++----------- ts/routes/graphs/reviews.ts | 4 +++- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/ts/lib/tslib/time.ts b/ts/lib/tslib/time.ts index 25d70eef3..a6285ff3f 100644 --- a/ts/lib/tslib/time.ts +++ b/ts/lib/tslib/time.ts @@ -170,20 +170,37 @@ export function dayLabel(daysStart: number, daysEnd: number): string { } else { return tr.statisticsDaysAgoSingle({ days: -daysStart }); } - } else { - // range - if (daysStart >= 0) { - return tr.statisticsInDaysRange({ - daysStart, - daysEnd: daysEnd - 1, - }); } else { - return tr.statisticsDaysAgoRange({ - daysStart: Math.abs(daysEnd - 1), - daysEnd: -daysStart, - }); + // range + if (daysStart >= 0) { + return tr.statisticsInDaysRange({ + daysStart, + daysEnd: daysEnd - 1, + }); + } else { + // For bins that cross or end at day 0, we need special handling + if (daysEnd > 0) { + // Bin crosses day 0: show from 0 to the oldest day (exclusive of endDay) + // If bin is [-5, 1), we want "0-4 days ago" + return tr.statisticsDaysAgoRange({ + daysStart: 0, + daysEnd: -daysStart - 1, + }); + } else { + // Bin is entirely in the past: show from newest to oldest + // For a bin [startDay, endDay), to show consecutive ranges like "0-4", "5-9", "10-14", + // we use endDay as the start of the range + // The bin width is daysEnd - daysStart, so daysEnd = daysStart + (width - 1) + // Example: bin [-10, -5) has width 5, should show "5-9 days ago" + const binWidth = Math.abs(daysEnd - daysStart); + const daysStartAbs = Math.abs(daysEnd); + return tr.statisticsDaysAgoRange({ + daysStart: daysStartAbs, + daysEnd: daysStartAbs + binWidth - 1, + }); + } + } } - } } /** Helper for converting Unix timestamps to date strings. */ diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 5235939fc..ed9c799d2 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -237,7 +237,9 @@ export function renderReviews( function tooltipText(d: BinType, cumulative: number): string { const startDay = Math.floor(d.x0!); - const endDay = Math.ceil(d.x1!); + // If bin ends at 0, treat it as including day 0 (so use 1 as endDay for dayLabel) + // For negative bins, use the bin end directly (dayLabel will handle the conversion) + const endDay = d.x1! === 0 ? 1 : d.x1! < 0 ? d.x1! : Math.ceil(d.x1!); const day = dayLabel(startDay, endDay); const totals = totalsForBin(d); const dayTotal = valueLabel(sum(totals)); From 9ff49ce1115f64be67f11a91ff4232ba3e1734dc Mon Sep 17 00:00:00 2001 From: junlu592 Date: Mon, 1 Dec 2025 13:21:02 +0100 Subject: [PATCH 15/32] fixed structure for ninja checks --- rslib/src/config/bool.rs | 7 ++--- ts/lib/tslib/time.ts | 52 ++++++++++++++++++------------------- ts/routes/graphs/reviews.ts | 9 ++++++- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index c76787cb0..8e4076175 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -59,11 +59,8 @@ pub enum BoolKey { /// This is a workaround for old clients that used ints to represent boolean /// values. For new config items, prefer using a bool directly. #[derive(Deserialize, Default)] -struct BoolLike( - #[serde(deserialize_with = "deserialize_bool_from_anything")] - #[allow(dead_code)] - bool, -); +#[allow(dead_code)] +struct BoolLike(#[serde(deserialize_with = "deserialize_bool_from_anything")] bool); impl Collection { pub fn get_config_bool(&self, key: BoolKey) -> bool { diff --git a/ts/lib/tslib/time.ts b/ts/lib/tslib/time.ts index a6285ff3f..b560f1546 100644 --- a/ts/lib/tslib/time.ts +++ b/ts/lib/tslib/time.ts @@ -170,37 +170,37 @@ export function dayLabel(daysStart: number, daysEnd: number): string { } else { return tr.statisticsDaysAgoSingle({ days: -daysStart }); } + } else { + // range + if (daysStart >= 0) { + return tr.statisticsInDaysRange({ + daysStart, + daysEnd: daysEnd - 1, + }); } else { - // range - if (daysStart >= 0) { - return tr.statisticsInDaysRange({ - daysStart, - daysEnd: daysEnd - 1, + // For bins that cross or end at day 0, we need special handling + if (daysEnd > 0) { + // Bin crosses day 0: show from 0 to the oldest day (exclusive of endDay) + // If bin is [-5, 1), we want "0-4 days ago" + return tr.statisticsDaysAgoRange({ + daysStart: 0, + daysEnd: -daysStart - 1, }); } else { - // For bins that cross or end at day 0, we need special handling - if (daysEnd > 0) { - // Bin crosses day 0: show from 0 to the oldest day (exclusive of endDay) - // If bin is [-5, 1), we want "0-4 days ago" - return tr.statisticsDaysAgoRange({ - daysStart: 0, - daysEnd: -daysStart - 1, - }); - } else { - // Bin is entirely in the past: show from newest to oldest - // For a bin [startDay, endDay), to show consecutive ranges like "0-4", "5-9", "10-14", - // we use endDay as the start of the range - // The bin width is daysEnd - daysStart, so daysEnd = daysStart + (width - 1) - // Example: bin [-10, -5) has width 5, should show "5-9 days ago" - const binWidth = Math.abs(daysEnd - daysStart); - const daysStartAbs = Math.abs(daysEnd); - return tr.statisticsDaysAgoRange({ - daysStart: daysStartAbs, - daysEnd: daysStartAbs + binWidth - 1, - }); - } + // Bin is entirely in the past: show from newest to oldest + // For a bin [startDay, endDay), to show consecutive ranges like "0-4", "5-9", "10-14", + // we use endDay as the start of the range + // The bin width is daysEnd - daysStart, so daysEnd = daysStart + (width - 1) + // Example: bin [-10, -5) has width 5, should show "5-9 days ago" + const binWidth = Math.abs(daysEnd - daysStart); + const daysStartAbs = Math.abs(daysEnd); + return tr.statisticsDaysAgoRange({ + daysStart: daysStartAbs, + daysEnd: daysStartAbs + binWidth - 1, + }); } } + } } /** Helper for converting Unix timestamps to date strings. */ diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index ed9c799d2..f26a9b157 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -239,7 +239,14 @@ export function renderReviews( const startDay = Math.floor(d.x0!); // If bin ends at 0, treat it as including day 0 (so use 1 as endDay for dayLabel) // For negative bins, use the bin end directly (dayLabel will handle the conversion) - const endDay = d.x1! === 0 ? 1 : d.x1! < 0 ? d.x1! : Math.ceil(d.x1!); + let endDay: number; + if (d.x1! === 0) { + endDay = 1; + } else if (d.x1! < 0) { + endDay = d.x1!; + } else { + endDay = Math.ceil(d.x1!); + } const day = dayLabel(startDay, endDay); const totals = totalsForBin(d); const dayTotal = valueLabel(sum(totals)); From 30d3ab1b9a15e801c148d590ed19105f8d83b5bb Mon Sep 17 00:00:00 2001 From: junlu592 Date: Mon, 1 Dec 2025 18:29:58 +0100 Subject: [PATCH 16/32] reverted changes made to unrelated files --- rslib/src/config/bool.rs | 7 +++++-- rslib/src/revlog/mod.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index 8e4076175..c76787cb0 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -59,8 +59,11 @@ pub enum BoolKey { /// This is a workaround for old clients that used ints to represent boolean /// values. For new config items, prefer using a bool directly. #[derive(Deserialize, Default)] -#[allow(dead_code)] -struct BoolLike(#[serde(deserialize_with = "deserialize_bool_from_anything")] bool); +struct BoolLike( + #[serde(deserialize_with = "deserialize_bool_from_anything")] + #[allow(dead_code)] + bool, +); impl Collection { pub fn get_config_bool(&self, key: BoolKey) -> bool { diff --git a/rslib/src/revlog/mod.rs b/rslib/src/revlog/mod.rs index 3e22890b1..fbb9b459a 100644 --- a/rslib/src/revlog/mod.rs +++ b/rslib/src/revlog/mod.rs @@ -161,7 +161,7 @@ impl Collection { ) -> Result<()> { let ease_factor = u32::from( card.memory_state - .map(|s| (s.difficulty_shifted() * 1000.) as u16) + .map(|s| ((s.difficulty_shifted() * 1000.) as u16)) .unwrap_or(card.ease_factor), ); let entry = RevlogEntry { From 6ebe80775ea376cea238704a9f170a89019fcedf Mon Sep 17 00:00:00 2001 From: junlu592 Date: Tue, 2 Dec 2025 11:29:32 +0100 Subject: [PATCH 17/32] reviewed code and simplified functions in review.ts --- ts/routes/graphs/reviews.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index f26a9b157..4651cd29f 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -90,37 +90,37 @@ export function renderReviews( ): TableDatum[] { const svg = select(svgElem); const trans = svg.transition().duration(600) as any; - - let xMax = 1; + + let xMax = 0; let xMin = 0; // cap max to selected range switch (range) { case GraphRange.Month: xMin = -30; - xMax = 0; break; case GraphRange.ThreeMonths: xMin = -89; - xMax = 0; break; case GraphRange.Year: xMin = -364; - xMax = 0; break; case GraphRange.AllTime: xMin = min(sourceData.reviewCount.keys())!; + xMax = 1; break; } const desiredBars = Math.min(70, Math.abs(xMin!)); + // Create initial scale to determine tick spacing let x = scaleLinear().domain([xMin!, xMax]); let thresholds = x.ticks(desiredBars); + // Adjust xMin to align with tick spacing so the oldest bin has the same width as others if (thresholds.length >= 2) { const spacing = thresholds[1] - thresholds[0]; - const partial = thresholds[0] - x.domain()[0]; + const partial = thresholds[0] - xMin!; if (spacing > 0 && partial > 0 && partial < spacing) { - const adjustedMin = thresholds[0] - spacing; - x = scaleLinear().domain([adjustedMin, xMax]); + xMin = thresholds[0] - spacing; + x = scaleLinear().domain([xMin, xMax]); thresholds = x.ticks(desiredBars); } } From 8135bc24ee0d921d156fed5be55c5611a0ed81be Mon Sep 17 00:00:00 2001 From: junlu592 Date: Tue, 2 Dec 2025 11:53:04 +0100 Subject: [PATCH 18/32] changed xMax, so today is included in month and 3months --- ts/routes/graphs/reviews.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 4651cd29f..839735767 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -91,7 +91,7 @@ export function renderReviews( const svg = select(svgElem); const trans = svg.transition().duration(600) as any; - let xMax = 0; + let xMax = 1; let xMin = 0; // cap max to selected range switch (range) { @@ -103,10 +103,10 @@ export function renderReviews( break; case GraphRange.Year: xMin = -364; + xMax = 0; break; case GraphRange.AllTime: xMin = min(sourceData.reviewCount.keys())!; - xMax = 1; break; } const desiredBars = Math.min(70, Math.abs(xMin!)); From 002b39e89dcc4818f3fa06ec5f582b11ca37fee7 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Tue, 2 Dec 2025 11:54:05 +0100 Subject: [PATCH 19/32] removed a whitespace --- ts/routes/graphs/reviews.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 839735767..c6a2abc5f 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -90,7 +90,6 @@ export function renderReviews( ): TableDatum[] { const svg = select(svgElem); const trans = svg.transition().duration(600) as any; - let xMax = 1; let xMin = 0; // cap max to selected range From c457b43a87228f1823cd96eace4a2abcd3aa37bd Mon Sep 17 00:00:00 2001 From: junlu592 Date: Tue, 2 Dec 2025 12:18:46 +0100 Subject: [PATCH 20/32] reverted unneccessary changes to xMax --- ts/routes/graphs/reviews.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index c6a2abc5f..8cb221886 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -90,7 +90,7 @@ export function renderReviews( ): TableDatum[] { const svg = select(svgElem); const trans = svg.transition().duration(600) as any; - let xMax = 1; + const xMax = 1; let xMin = 0; // cap max to selected range switch (range) { @@ -102,7 +102,6 @@ export function renderReviews( break; case GraphRange.Year: xMin = -364; - xMax = 0; break; case GraphRange.AllTime: xMin = min(sourceData.reviewCount.keys())!; From 0c2c5c6a1c8429ae952adda68a84c2eb55eb8045 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Tue, 2 Dec 2025 16:28:00 +0100 Subject: [PATCH 21/32] simplified code in time.ts and review.rs and pass bin boundaries directly to time.ts --- ts/lib/tslib/time.ts | 32 +++++++++++--------------------- ts/routes/graphs/reviews.ts | 13 +++---------- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/ts/lib/tslib/time.ts b/ts/lib/tslib/time.ts index b560f1546..05bc45563 100644 --- a/ts/lib/tslib/time.ts +++ b/ts/lib/tslib/time.ts @@ -177,28 +177,18 @@ export function dayLabel(daysStart: number, daysEnd: number): string { daysStart, daysEnd: daysEnd - 1, }); + } else if (daysEnd <= 0) { + // Past: [-10, -5) -> "5-9 days ago" + return tr.statisticsDaysAgoRange({ + daysStart: Math.abs(daysEnd), + daysEnd: Math.abs(daysStart) - 1, + }); } else { - // For bins that cross or end at day 0, we need special handling - if (daysEnd > 0) { - // Bin crosses day 0: show from 0 to the oldest day (exclusive of endDay) - // If bin is [-5, 1), we want "0-4 days ago" - return tr.statisticsDaysAgoRange({ - daysStart: 0, - daysEnd: -daysStart - 1, - }); - } else { - // Bin is entirely in the past: show from newest to oldest - // For a bin [startDay, endDay), to show consecutive ranges like "0-4", "5-9", "10-14", - // we use endDay as the start of the range - // The bin width is daysEnd - daysStart, so daysEnd = daysStart + (width - 1) - // Example: bin [-10, -5) has width 5, should show "5-9 days ago" - const binWidth = Math.abs(daysEnd - daysStart); - const daysStartAbs = Math.abs(daysEnd); - return tr.statisticsDaysAgoRange({ - daysStart: daysStartAbs, - daysEnd: daysStartAbs + binWidth - 1, - }); - } + // Crosses zero: [-5, 1) -> "0-4 days ago" + return tr.statisticsDaysAgoRange({ + daysStart: 0, + daysEnd: Math.abs(daysStart) - 1, + }); } } } diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 8cb221886..a32089a3e 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -234,17 +234,10 @@ export function renderReviews( } function tooltipText(d: BinType, cumulative: number): string { + // Convert bin boundaries [x0, x1) for dayLabel + // If bin ends at 0, treat it as crossing zero so day 0 is included const startDay = Math.floor(d.x0!); - // If bin ends at 0, treat it as including day 0 (so use 1 as endDay for dayLabel) - // For negative bins, use the bin end directly (dayLabel will handle the conversion) - let endDay: number; - if (d.x1! === 0) { - endDay = 1; - } else if (d.x1! < 0) { - endDay = d.x1!; - } else { - endDay = Math.ceil(d.x1!); - } + const endDay = d.x1! === 0 ? 1 : d.x1!; const day = dayLabel(startDay, endDay); const totals = totalsForBin(d); const dayTotal = valueLabel(sum(totals)); From 6e341d5e0722e9da6c074d6a196e84c18f73a31c Mon Sep 17 00:00:00 2001 From: junlu592 Date: Tue, 9 Dec 2025 13:17:59 +0100 Subject: [PATCH 22/32] fixed labels for bins, and made sure year is 365 days --- ts/lib/tslib/time.ts | 11 ++--------- ts/routes/graphs/reviews.ts | 8 +++++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/ts/lib/tslib/time.ts b/ts/lib/tslib/time.ts index 05bc45563..25d70eef3 100644 --- a/ts/lib/tslib/time.ts +++ b/ts/lib/tslib/time.ts @@ -177,17 +177,10 @@ export function dayLabel(daysStart: number, daysEnd: number): string { daysStart, daysEnd: daysEnd - 1, }); - } else if (daysEnd <= 0) { - // Past: [-10, -5) -> "5-9 days ago" - return tr.statisticsDaysAgoRange({ - daysStart: Math.abs(daysEnd), - daysEnd: Math.abs(daysStart) - 1, - }); } else { - // Crosses zero: [-5, 1) -> "0-4 days ago" return tr.statisticsDaysAgoRange({ - daysStart: 0, - daysEnd: Math.abs(daysStart) - 1, + daysStart: Math.abs(daysEnd - 1), + daysEnd: -daysStart, }); } } diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index a32089a3e..f8832525b 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -108,6 +108,8 @@ export function renderReviews( break; } const desiredBars = Math.min(70, Math.abs(xMin!)); + const shouldCapRange = range !== GraphRange.AllTime; + const originalXMin = shouldCapRange ? xMin! : undefined; // Create initial scale to determine tick spacing let x = scaleLinear().domain([xMin!, xMax]); @@ -117,7 +119,11 @@ export function renderReviews( const spacing = thresholds[1] - thresholds[0]; const partial = thresholds[0] - xMin!; if (spacing > 0 && partial > 0 && partial < spacing) { - xMin = thresholds[0] - spacing; + const adjustedMin = thresholds[0] - spacing; + // Don't extend beyond the original range limit for fixed ranges + xMin = shouldCapRange + ? Math.max(adjustedMin, originalXMin!) + : adjustedMin; x = scaleLinear().domain([xMin, xMax]); thresholds = x.ticks(desiredBars); } From a2be2b964f5f867bf7574188d37cd077b01c5318 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Tue, 9 Dec 2025 14:56:30 +0100 Subject: [PATCH 23/32] fixed even bin sizes (0-4 days and 360-364 days) for year review graph --- ts/routes/graphs/reviews.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index f8832525b..94583b45f 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -90,7 +90,7 @@ export function renderReviews( ): TableDatum[] { const svg = select(svgElem); const trans = svg.transition().duration(600) as any; - const xMax = 1; + let xMax = 1; let xMin = 0; // cap max to selected range switch (range) { @@ -119,21 +119,20 @@ export function renderReviews( const spacing = thresholds[1] - thresholds[0]; const partial = thresholds[0] - xMin!; if (spacing > 0 && partial > 0 && partial < spacing) { - const adjustedMin = thresholds[0] - spacing; - // Don't extend beyond the original range limit for fixed ranges - xMin = shouldCapRange - ? Math.max(adjustedMin, originalXMin!) - : adjustedMin; + xMin = Math.max(thresholds[0] - spacing, shouldCapRange ? originalXMin! : -Infinity); x = scaleLinear().domain([xMin, xMax]); thresholds = x.ticks(desiredBars); } } + + // For Year, shift thresholds forward by one day to make first bin 0-4 instead of 0-5 + if (range === GraphRange.Year) { + thresholds = [...new Set(thresholds.map(t => Math.min(t + 1, 1)))].sort((a, b) => a - b); + } const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; let bins = bin() - .value((m) => { - return m[0]; - }) + .value((m) => m[0]) .domain(x.domain() as any) .thresholds(thresholds)(sourceMap.entries() as any); From bc6bd05fee17ec3909dd8b8e5b90ecf422e66c3e Mon Sep 17 00:00:00 2001 From: junlu592 Date: Tue, 9 Dec 2025 15:01:31 +0100 Subject: [PATCH 24/32] ninja check fix, let -> const and removed empty line --- ts/routes/graphs/reviews.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 94583b45f..e67675069 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -90,7 +90,7 @@ export function renderReviews( ): TableDatum[] { const svg = select(svgElem); const trans = svg.transition().duration(600) as any; - let xMax = 1; + const xMax = 1; let xMin = 0; // cap max to selected range switch (range) { @@ -124,7 +124,6 @@ export function renderReviews( thresholds = x.ticks(desiredBars); } } - // For Year, shift thresholds forward by one day to make first bin 0-4 instead of 0-5 if (range === GraphRange.Year) { thresholds = [...new Set(thresholds.map(t => Math.min(t + 1, 1)))].sort((a, b) => a - b); From 722c5a58a58fa960b21b484ab9fd069fe7f9e6cb Mon Sep 17 00:00:00 2001 From: junlu592 Date: Wed, 10 Dec 2025 10:50:30 +0100 Subject: [PATCH 25/32] added comments to review.ts --- ts/routes/graphs/reviews.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index e67675069..933f56d20 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -113,12 +113,15 @@ export function renderReviews( // Create initial scale to determine tick spacing let x = scaleLinear().domain([xMin!, xMax]); + // Generate thresholds: these define where bins are split (e.g., [-5, -10, -15, ...]) + // D3's bin() will create bins between consecutive thresholds: [threshold[i], threshold[i+1]) let thresholds = x.ticks(desiredBars); // Adjust xMin to align with tick spacing so the oldest bin has the same width as others if (thresholds.length >= 2) { const spacing = thresholds[1] - thresholds[0]; const partial = thresholds[0] - xMin!; if (spacing > 0 && partial > 0 && partial < spacing) { + // Extend xMin backward to align with tick spacing, but cap at originalXMin for fixed ranges xMin = Math.max(thresholds[0] - spacing, shouldCapRange ? originalXMin! : -Infinity); x = scaleLinear().domain([xMin, xMax]); thresholds = x.ticks(desiredBars); From 8ab9e54a62eb2c63ef318c56e5600ba35d0500fa Mon Sep 17 00:00:00 2001 From: junlu592 Date: Wed, 10 Dec 2025 12:49:40 +0100 Subject: [PATCH 26/32] bin merging logic removed, since totals are correctly calculated anyways --- ts/routes/graphs/reviews.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 933f56d20..f40b532e4 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -133,24 +133,12 @@ export function renderReviews( } const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; + // Create bins using the thresholds: each bin contains data points between consecutive thresholds let bins = bin() - .value((m) => m[0]) - .domain(x.domain() as any) - .thresholds(thresholds)(sourceMap.entries() as any); + .value((m) => m[0]) // Extract the day number from each map entry [day, data] + .domain(x.domain() as any) // Bins are constrained to this domain [xMin, xMax] + .thresholds(thresholds)(sourceMap.entries() as any); // Use computed thresholds to split data into bins - if (bins.length > 1 && thresholds.length > 1) { - const lastBin = bins[bins.length - 1]; - const prevBin = bins[bins.length - 2]; - const nominalWidth = thresholds[1] - thresholds[0]; - const lastWidth = lastBin.x1! - lastBin.x0!; - if (lastBin.x1! > 0 && lastWidth < nominalWidth * 0.75) { - for (const entry of lastBin) { - prevBin.push(entry); - } - prevBin.x1 = lastBin.x1; - bins = bins.slice(0, -1); - } - } // empty graph? const totalDays = sum(bins, (bin) => bin.length); From ce9b087190d5dda41de5c9ccd22054713e5ffbf0 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Wed, 10 Dec 2025 12:52:56 +0100 Subject: [PATCH 27/32] corrections from ninja check --- ts/routes/graphs/reviews.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index f40b532e4..df1c03759 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -134,12 +134,11 @@ export function renderReviews( const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; // Create bins using the thresholds: each bin contains data points between consecutive thresholds - let bins = bin() + const bins = bin() .value((m) => m[0]) // Extract the day number from each map entry [day, data] .domain(x.domain() as any) // Bins are constrained to this domain [xMin, xMax] .thresholds(thresholds)(sourceMap.entries() as any); // Use computed thresholds to split data into bins - // empty graph? const totalDays = sum(bins, (bin) => bin.length); if (!totalDays) { From d737f5da72eb75abfa5de7300892e3e42e056852 Mon Sep 17 00:00:00 2001 From: Junia Mannervik <72745451+JMannervik@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:47:21 +0100 Subject: [PATCH 28/32] Update ts/routes/graphs/reviews.ts Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> --- ts/routes/graphs/reviews.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index df1c03759..ae33cc780 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -109,7 +109,7 @@ export function renderReviews( } const desiredBars = Math.min(70, Math.abs(xMin!)); const shouldCapRange = range !== GraphRange.AllTime; - const originalXMin = shouldCapRange ? xMin! : undefined; + const originalXMin = xMin!; // Create initial scale to determine tick spacing let x = scaleLinear().domain([xMin!, xMax]); From 7a883600ab7310e2f900325d6dc5e9d4d268901c Mon Sep 17 00:00:00 2001 From: Junia Mannervik <72745451+JMannervik@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:48:02 +0100 Subject: [PATCH 29/32] Update ts/routes/graphs/reviews.ts Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> --- ts/routes/graphs/reviews.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index ae33cc780..c4e2640a4 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -128,7 +128,7 @@ export function renderReviews( } } // For Year, shift thresholds forward by one day to make first bin 0-4 instead of 0-5 - if (range === GraphRange.Year) { + if (range === GraphRange.Year || range === GraphRange.AllTime) { thresholds = [...new Set(thresholds.map(t => Math.min(t + 1, 1)))].sort((a, b) => a - b); } From d00dc07613855fc103f4d4be8991408b6693fcc5 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Fri, 12 Dec 2025 14:38:23 +0100 Subject: [PATCH 30/32] adjusted last bin labeling with xmin --- ts/routes/graphs/reviews.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index df1c03759..f23ad7fdc 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -109,6 +109,8 @@ export function renderReviews( } const desiredBars = Math.min(70, Math.abs(xMin!)); const shouldCapRange = range !== GraphRange.AllTime; + // Store original xMin before any adjustments (needed for periodDays calculation) + const originalXMinBeforeAdjustment = xMin!; const originalXMin = shouldCapRange ? xMin! : undefined; // Create initial scale to determine tick spacing @@ -230,7 +232,11 @@ export function renderReviews( function tooltipText(d: BinType, cumulative: number): string { // Convert bin boundaries [x0, x1) for dayLabel // If bin ends at 0, treat it as crossing zero so day 0 is included - const startDay = Math.floor(d.x0!); + // For the first (oldest) bin, use the original xMin to ensure labels match the intended range + const isFirstBin = bins.length > 0 && d.x0 === bins[0].x0; + const startDay = isFirstBin + ? originalXMinBeforeAdjustment + : Math.floor(d.x0!); const endDay = d.x1! === 0 ? 1 : d.x1!; const day = dayLabel(startDay, endDay); const totals = totalsForBin(d); @@ -347,7 +353,11 @@ export function renderReviews( }) .on("mouseout", hideTooltip); - const periodDays = -xMin + 1; + // Calculate periodDays from the actual data range, not the adjusted xMin + // For AllTime, xMin might be extended for bin alignment, so use the original xMin (actual oldest data) + // For fixed ranges, use the original xMin before adjustment (which matches the displayed range) + const actualXMin = originalXMinBeforeAdjustment; + const periodDays = -actualXMin + 1; const studiedDays = sum(bins, (bin) => bin.length); const studiedPercent = (studiedDays / periodDays) * 100; const total = yCumMax; From d1f0ef6dfec851a78d3e42a71595144834faace3 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Sun, 14 Dec 2025 10:56:04 +0100 Subject: [PATCH 31/32] suggested changes to minimize difference from main --- ts/routes/graphs/reviews.ts | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index d66496687..b33232b25 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -108,36 +108,32 @@ export function renderReviews( break; } const desiredBars = Math.min(70, Math.abs(xMin!)); - const shouldCapRange = range !== GraphRange.AllTime; + const unboundRange = range == GraphRange.AllTime; const originalXMin = xMin!; // Create initial scale to determine tick spacing let x = scaleLinear().domain([xMin!, xMax]); - // Generate thresholds: these define where bins are split (e.g., [-5, -10, -15, ...]) - // D3's bin() will create bins between consecutive thresholds: [threshold[i], threshold[i+1]) let thresholds = x.ticks(desiredBars); - // Adjust xMin to align with tick spacing so the oldest bin has the same width as others - if (thresholds.length >= 2) { + // For unbound ranges, extend xMin backward so that the oldest bin has the same width as others + if (unboundRange && thresholds.length >= 2) { const spacing = thresholds[1] - thresholds[0]; const partial = thresholds[0] - xMin!; if (spacing > 0 && partial > 0 && partial < spacing) { - // Extend xMin backward to align with tick spacing, but cap at originalXMin for fixed ranges - xMin = Math.max(thresholds[0] - spacing, shouldCapRange ? originalXMin! : -Infinity); + xMin = thresholds[0] - spacing; x = scaleLinear().domain([xMin, xMax]); thresholds = x.ticks(desiredBars); } } - // For Year, shift thresholds forward by one day to make first bin 0-4 instead of 0-5 + // For Year & All Time, shift thresholds forward by one day to make first bin 0-4 instead of 0-5 if (range === GraphRange.Year || range === GraphRange.AllTime) { thresholds = [...new Set(thresholds.map(t => Math.min(t + 1, 1)))].sort((a, b) => a - b); } const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount; - // Create bins using the thresholds: each bin contains data points between consecutive thresholds const bins = bin() - .value((m) => m[0]) // Extract the day number from each map entry [day, data] - .domain(x.domain() as any) // Bins are constrained to this domain [xMin, xMax] - .thresholds(thresholds)(sourceMap.entries() as any); // Use computed thresholds to split data into bins + .value((m) => m[0]) + .domain(x.domain() as any) + .thresholds(thresholds)(sourceMap.entries() as any); // empty graph? const totalDays = sum(bins, (bin) => bin.length); @@ -232,9 +228,7 @@ export function renderReviews( // If bin ends at 0, treat it as crossing zero so day 0 is included // For the first (oldest) bin, use the original xMin to ensure labels match the intended range const isFirstBin = bins.length > 0 && d.x0 === bins[0].x0; - const startDay = isFirstBin - ? originalXMin - : Math.floor(d.x0!); + const startDay = isFirstBin ? originalXMin : Math.floor(d.x0!); const endDay = d.x1! === 0 ? 1 : d.x1!; const day = dayLabel(startDay, endDay); const totals = totalsForBin(d); @@ -351,10 +345,9 @@ export function renderReviews( }) .on("mouseout", hideTooltip); - // Calculate periodDays from the actual data range, not the adjusted xMin - // For AllTime, xMin might be extended for bin alignment, so use the original xMin (actual oldest data) - // For fixed ranges, use the original xMin before adjustment (which matches the displayed range) - const periodDays = -originalXMin + 1; + // The xMin might be extended for bin alignment, so use the original xMin + const actualXMin = originalXMin; + const periodDays = -actualXMin + 1; const studiedDays = sum(bins, (bin) => bin.length); const studiedPercent = (studiedDays / periodDays) * 100; const total = yCumMax; From 8ec68182afdb78b08ee36ec73d1358c212f9d360 Mon Sep 17 00:00:00 2001 From: junlu592 Date: Sun, 14 Dec 2025 12:17:38 +0100 Subject: [PATCH 32/32] small review changes --- ts/routes/graphs/reviews.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index b33232b25..64cbdb63b 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -90,6 +90,7 @@ export function renderReviews( ): TableDatum[] { const svg = select(svgElem); const trans = svg.transition().duration(600) as any; + const xMax = 1; let xMin = 0; // cap max to selected range @@ -346,8 +347,7 @@ export function renderReviews( .on("mouseout", hideTooltip); // The xMin might be extended for bin alignment, so use the original xMin - const actualXMin = originalXMin; - const periodDays = -actualXMin + 1; + const periodDays = -originalXMin + 1; const studiedDays = sum(bins, (bin) => bin.length); const studiedPercent = (studiedDays / periodDays) * 100; const total = yCumMax;
${day}${dayTotal}