refactor(stats): drop animePerDay from trends response in favor of librarySummary

This commit is contained in:
2026-04-09 22:16:17 -07:00
parent f91c600ed0
commit 8751ffd6c8
3 changed files with 16 additions and 76 deletions

View File

@@ -166,14 +166,20 @@ const TRENDS_DASHBOARD = {
ratios: { ratios: {
lookupsPerHundred: [{ label: 'Mar 1', value: 5 }], lookupsPerHundred: [{ label: 'Mar 1', value: 5 }],
}, },
animePerDay: { librarySummary: [
episodes: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 1 }], {
watchTime: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 25 }], title: 'Little Witch Academia',
cards: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 5 }], watchTimeMin: 25,
words: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 300 }], videos: 1,
lookups: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 15 }], sessions: 1,
lookupsPerHundred: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 5 }], cards: 5,
}, words: 300,
lookups: 15,
lookupsPerHundred: 5,
firstWatched: 20_000,
lastWatched: 20_000,
},
],
animeCumulative: { animeCumulative: {
watchTime: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 25 }], watchTime: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 25 }],
episodes: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 1 }], episodes: [{ epochDay: 20_000, animeTitle: 'Little Witch Academia', value: 1 }],
@@ -598,7 +604,7 @@ describe('stats server API routes', () => {
const body = await res.json(); const body = await res.json();
assert.deepEqual(seenArgs, ['90d', 'month']); assert.deepEqual(seenArgs, ['90d', 'month']);
assert.deepEqual(body.activity.watchTime, TRENDS_DASHBOARD.activity.watchTime); assert.deepEqual(body.activity.watchTime, TRENDS_DASHBOARD.activity.watchTime);
assert.deepEqual(body.animePerDay.watchTime, TRENDS_DASHBOARD.animePerDay.watchTime); assert.deepEqual(body.librarySummary, TRENDS_DASHBOARD.librarySummary);
}); });
it('GET /api/stats/trends/dashboard accepts 365d range', async () => { it('GET /api/stats/trends/dashboard accepts 365d range', async () => {

View File

@@ -687,7 +687,7 @@ test('getTrendsDashboard returns chart-ready aggregated series', () => {
assert.equal(dashboard.progress.watchTime[1]?.value, 75); assert.equal(dashboard.progress.watchTime[1]?.value, 75);
assert.equal(dashboard.progress.lookups[1]?.value, 18); assert.equal(dashboard.progress.lookups[1]?.value, 18);
assert.equal(dashboard.ratios.lookupsPerHundred[0]?.value, +((8 / 120) * 100).toFixed(1)); assert.equal(dashboard.ratios.lookupsPerHundred[0]?.value, +((8 / 120) * 100).toFixed(1));
assert.equal(dashboard.animePerDay.watchTime[0]?.animeTitle, 'Trend Dashboard Anime'); assert.equal(dashboard.librarySummary[0]?.title, 'Trend Dashboard Anime');
assert.equal(dashboard.animeCumulative.watchTime[1]?.value, 75); assert.equal(dashboard.animeCumulative.watchTime[1]?.value, 75);
assert.equal( assert.equal(
dashboard.patterns.watchTimeByDayOfWeek.reduce((sum, point) => sum + point.value, 0), dashboard.patterns.watchTimeByDayOfWeek.reduce((sum, point) => sum + point.value, 0),

View File

@@ -74,14 +74,6 @@ export interface TrendsDashboardQueryResult {
ratios: { ratios: {
lookupsPerHundred: TrendChartPoint[]; lookupsPerHundred: TrendChartPoint[];
}; };
animePerDay: {
episodes: TrendPerAnimePoint[];
watchTime: TrendPerAnimePoint[];
cards: TrendPerAnimePoint[];
words: TrendPerAnimePoint[];
lookups: TrendPerAnimePoint[];
lookupsPerHundred: TrendPerAnimePoint[];
};
animeCumulative: { animeCumulative: {
watchTime: TrendPerAnimePoint[]; watchTime: TrendPerAnimePoint[];
episodes: TrendPerAnimePoint[]; episodes: TrendPerAnimePoint[];
@@ -315,61 +307,6 @@ function buildLookupsPerHundredWords(
}); });
} }
function buildPerAnimeFromSessions(
sessions: TrendSessionMetricRow[],
getValue: (session: TrendSessionMetricRow) => number,
): TrendPerAnimePoint[] {
const byAnime = new Map<string, Map<number, number>>();
for (const session of sessions) {
const animeTitle = resolveTrendAnimeTitle(session);
const epochDay = session.epochDay;
const dayMap = byAnime.get(animeTitle) ?? new Map();
dayMap.set(epochDay, (dayMap.get(epochDay) ?? 0) + getValue(session));
byAnime.set(animeTitle, dayMap);
}
const result: TrendPerAnimePoint[] = [];
for (const [animeTitle, dayMap] of byAnime) {
for (const [epochDay, value] of dayMap) {
result.push({ epochDay, animeTitle, value });
}
}
return result;
}
function buildLookupsPerHundredPerAnime(sessions: TrendSessionMetricRow[]): TrendPerAnimePoint[] {
const lookups = new Map<string, Map<number, number>>();
const words = new Map<string, Map<number, number>>();
for (const session of sessions) {
const animeTitle = resolveTrendAnimeTitle(session);
const epochDay = session.epochDay;
const lookupMap = lookups.get(animeTitle) ?? new Map();
lookupMap.set(epochDay, (lookupMap.get(epochDay) ?? 0) + session.yomitanLookupCount);
lookups.set(animeTitle, lookupMap);
const wordMap = words.get(animeTitle) ?? new Map();
wordMap.set(epochDay, (wordMap.get(epochDay) ?? 0) + getTrendSessionWordCount(session));
words.set(animeTitle, wordMap);
}
const result: TrendPerAnimePoint[] = [];
for (const [animeTitle, dayMap] of lookups) {
const wordMap = words.get(animeTitle) ?? new Map();
for (const [epochDay, lookupCount] of dayMap) {
const wordCount = wordMap.get(epochDay) ?? 0;
result.push({
epochDay,
animeTitle,
value: wordCount > 0 ? +((lookupCount / wordCount) * 100).toFixed(1) : 0,
});
}
}
return result;
}
function buildCumulativePerAnime(points: TrendPerAnimePoint[]): TrendPerAnimePoint[] { function buildCumulativePerAnime(points: TrendPerAnimePoint[]): TrendPerAnimePoint[] {
const byAnime = new Map<string, Map<number, number>>(); const byAnime = new Map<string, Map<number, number>>();
const allDays = new Set<number>(); const allDays = new Set<number>();
@@ -760,8 +697,6 @@ export function getTrendsDashboard(
titlesByVideoId, titlesByVideoId,
(rollup) => rollup.totalTokensSeen, (rollup) => rollup.totalTokensSeen,
), ),
lookups: buildPerAnimeFromSessions(sessions, (session) => session.yomitanLookupCount),
lookupsPerHundred: buildLookupsPerHundredPerAnime(sessions),
}; };
return { return {
@@ -788,7 +723,6 @@ export function getTrendsDashboard(
ratios: { ratios: {
lookupsPerHundred: buildLookupsPerHundredWords(sessions, groupBy), lookupsPerHundred: buildLookupsPerHundredWords(sessions, groupBy),
}, },
animePerDay,
animeCumulative: { animeCumulative: {
watchTime: buildCumulativePerAnime(animePerDay.watchTime), watchTime: buildCumulativePerAnime(animePerDay.watchTime),
episodes: buildCumulativePerAnime(animePerDay.episodes), episodes: buildCumulativePerAnime(animePerDay.episodes),