mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 16:46:06 -08:00
perf(stats): speed up dashboard queries on large databases (#322)
This commit is contained in:
1 parent
8976d78390
commit
d8c2315405
18 files changed
+701
-318
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTrends, type TimeRange, type GroupBy } from '../../hooks/useTrends';
|
||||
import { DateRangeSelector } from './DateRangeSelector';
|
||||
import { TrendChart } from './TrendChart';
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type MaxTitlesMode,
|
||||
} from './anime-visibility';
|
||||
import { LibrarySummarySection } from './LibrarySummarySection';
|
||||
import { buildAnimeCumulative } from './anime-cumulative';
|
||||
|
||||
const MAX_TITLES_MODE_LABELS: Record<MaxTitlesMode, string> = {
|
||||
recent: 'most recent',
|
||||
@@ -156,6 +157,10 @@ export function TrendsTab() {
|
||||
const [maxTitles, setMaxTitles] = useState<number | null>(() => loadMaxTitles());
|
||||
const [maxTitlesMode, setMaxTitlesMode] = useState<MaxTitlesMode>(() => loadMaxTitlesMode());
|
||||
const { data, loading, error } = useTrends(range, groupBy, showEmptyDays);
|
||||
const animeCumulative = useMemo(
|
||||
() => (data ? buildAnimeCumulative(data.animeDaily) : null),
|
||||
[data],
|
||||
);
|
||||
|
||||
const updateHiddenAnime = (next: Set<string>) => {
|
||||
setHiddenAnime(next);
|
||||
@@ -187,30 +192,21 @@ export function TrendsTab() {
|
||||
|
||||
if (loading) return <div className="text-ctp-overlay2 p-4">Loading...</div>;
|
||||
if (error) return <div className="text-ctp-red p-4">Error: {error}</div>;
|
||||
if (!data) return null;
|
||||
if (!data || !animeCumulative) return null;
|
||||
|
||||
const animeTitles = buildAnimeVisibilityOptions([
|
||||
data.animeCumulative.episodes,
|
||||
data.animeCumulative.cards,
|
||||
data.animeCumulative.words,
|
||||
data.animeCumulative.watchTime,
|
||||
animeCumulative.episodes,
|
||||
animeCumulative.cards,
|
||||
animeCumulative.words,
|
||||
animeCumulative.watchTime,
|
||||
]);
|
||||
const activeHiddenAnime = pruneHiddenAnime(hiddenAnime, animeTitles);
|
||||
|
||||
const filteredAnimeProgress = filterHiddenAnimeData(
|
||||
data.animeCumulative.episodes,
|
||||
activeHiddenAnime,
|
||||
);
|
||||
const filteredCardsProgress = filterHiddenAnimeData(
|
||||
data.animeCumulative.cards,
|
||||
activeHiddenAnime,
|
||||
);
|
||||
const filteredWordsProgress = filterHiddenAnimeData(
|
||||
data.animeCumulative.words,
|
||||
activeHiddenAnime,
|
||||
);
|
||||
const filteredAnimeProgress = filterHiddenAnimeData(animeCumulative.episodes, activeHiddenAnime);
|
||||
const filteredCardsProgress = filterHiddenAnimeData(animeCumulative.cards, activeHiddenAnime);
|
||||
const filteredWordsProgress = filterHiddenAnimeData(animeCumulative.words, activeHiddenAnime);
|
||||
const filteredWatchTimeProgress = filterHiddenAnimeData(
|
||||
data.animeCumulative.watchTime,
|
||||
animeCumulative.watchTime,
|
||||
activeHiddenAnime,
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { buildCumulativePerAnime } from './anime-cumulative';
|
||||
|
||||
test('buildCumulativePerAnime carries each title forward to the last active day', () => {
|
||||
const result = buildCumulativePerAnime([
|
||||
{ epochDay: 103, animeTitle: 'B', value: 4 },
|
||||
{ epochDay: 100, animeTitle: 'A', value: 10 },
|
||||
{ epochDay: 102, animeTitle: 'A', value: 5 },
|
||||
{ epochDay: 102, animeTitle: 'A', value: 1 },
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
result
|
||||
.filter((point) => point.animeTitle === 'A')
|
||||
.map((point) => [point.epochDay, point.value]),
|
||||
[
|
||||
[100, 10],
|
||||
[101, 10],
|
||||
[102, 16],
|
||||
[103, 16],
|
||||
],
|
||||
);
|
||||
// A title starts at its own first active day, not the earliest day overall.
|
||||
assert.deepEqual(
|
||||
result
|
||||
.filter((point) => point.animeTitle === 'B')
|
||||
.map((point) => [point.epochDay, point.value]),
|
||||
[[103, 4]],
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCumulativePerAnime returns nothing for no points', () => {
|
||||
assert.deepEqual(buildCumulativePerAnime([]), []);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { TrendsDashboardData } from '../../types/stats';
|
||||
import type { PerAnimeDataPoint } from './StackedTrendChart';
|
||||
|
||||
type AnimeSeries = TrendsDashboardData['animeDaily'];
|
||||
|
||||
/**
|
||||
* Turns sparse per-title daily values into running totals: one point per
|
||||
* title for every calendar day from its first active day to the last active
|
||||
* day of any title, so the stacked area charts carry each total forward.
|
||||
*/
|
||||
export function buildCumulativePerAnime(points: PerAnimeDataPoint[]): PerAnimeDataPoint[] {
|
||||
const byAnime = new Map<string, Map<number, number>>();
|
||||
let maxDay = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const point of points) {
|
||||
const dayMap = byAnime.get(point.animeTitle) ?? new Map<number, number>();
|
||||
dayMap.set(point.epochDay, (dayMap.get(point.epochDay) ?? 0) + point.value);
|
||||
byAnime.set(point.animeTitle, dayMap);
|
||||
maxDay = Math.max(maxDay, point.epochDay);
|
||||
}
|
||||
|
||||
const result: PerAnimeDataPoint[] = [];
|
||||
for (const [animeTitle, dayMap] of byAnime) {
|
||||
const firstDay = Math.min(...dayMap.keys());
|
||||
let cumulative = 0;
|
||||
for (let epochDay = firstDay; epochDay <= maxDay; epochDay += 1) {
|
||||
cumulative += dayMap.get(epochDay) ?? 0;
|
||||
result.push({ epochDay, animeTitle, value: cumulative });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function buildAnimeCumulative(daily: AnimeSeries): AnimeSeries {
|
||||
return {
|
||||
watchTime: buildCumulativePerAnime(daily.watchTime),
|
||||
episodes: buildCumulativePerAnime(daily.episodes),
|
||||
cards: buildCumulativePerAnime(daily.cards),
|
||||
words: buildCumulativePerAnime(daily.words),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user