From 7f9d7c37f3e478af7928e0b27801f76fcdb77a27 Mon Sep 17 00:00:00 2001 From: tdgao Date: Tue, 14 Jul 2026 11:03:25 -0700 Subject: [PATCH] feat: implement showing "same day last week" for yesterday timeframe instead of prev period --- .../AnalyticsChartControls.vue | 10 +++++-- .../analytics-chart-header/index.vue | 2 ++ .../use-analytics-chart-legend.ts | 11 ++++++-- .../AnalyticsChartTooltip.vue | 8 +++++- .../analytics-chart-plot/index.vue | 3 ++ .../use-analytics-chart-interactions.ts | 6 +++- .../analytics-chart/index.vue | 7 +++++ .../analytics-dashboard/analytics-messages.ts | 20 +++++++++++++ .../stat-cards/StatCard.vue | 17 +++++++++-- .../stat-cards/StatCards.vue | 6 ++++ .../analytics/analytics-data-utils.ts | 28 +++++++++++++++---- .../src/providers/analytics/analytics.ts | 19 +++++++++++-- 12 files changed, 120 insertions(+), 17 deletions(-) diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartControls.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartControls.vue index c1804f9ca0..e3f8afe751 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartControls.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartControls.vue @@ -39,7 +39,7 @@ ref="controlsMenuPanel" role="dialog" :aria-label="formatMessage(analyticsChartMessages.controlsDialogAria)" - class="mt-1 flex w-[228px] max-w-[calc(100vw_-_2rem)] flex-col overflow-hidden rounded-[14px] border border-solid border-surface-4 bg-surface-3 text-sm shadow-2xl" + class="mt-1 flex w-[238px] max-w-[calc(100vw_-_2rem)] flex-col overflow-hidden rounded-[14px] border border-solid border-surface-4 bg-surface-3 text-sm shadow-2xl" >
{{ activeControlCountLabel }} @@ -68,7 +68,7 @@ >
@@ -169,6 +169,7 @@ const props = defineProps<{ containerHeight: number pinned: boolean ratioMode: boolean + isSameDayLastWeekComparison: boolean capitalizeLabels: boolean shiftKeyPressed: boolean }>() @@ -180,6 +181,11 @@ const emit = defineEmits<{ }>() const { formatMessage } = useVIntl() +const previousPeriodMessage = computed(() => + props.isSameDayLastWeekComparison + ? analyticsChartMessages.sameDayLastWeekShort + : analyticsChartMessages.previousPeriodShort, +) function onEntryClick(event: MouseEvent, entry: AnalyticsChartTooltipEntry) { if (entry.toggleDisabled && !event.shiftKey) return diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue index 91fe11d48f..ecce24cfde 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue @@ -69,6 +69,7 @@ :container-height="containerSize.height" :pinned="isHoverPinned" :ratio-mode="isRatioMode" + :is-same-day-last-week-comparison="isSameDayLastWeekComparison" :capitalize-labels="shouldCapitalizeDatasetLabels" :shift-key-pressed="isShiftKeyPressed" @entry-click="(datasetId, shiftKey) => emit('entry-click', datasetId, shiftKey)" @@ -120,6 +121,7 @@ const props = defineProps<{ fetchRequest: Labrinth.Analytics.v3.FetchRequest | null sliceCount: number shouldShowPreviousPeriod: boolean + isSameDayLastWeekComparison: boolean allChartDatasets: ChartDataset[] currentLegendEntries: AnalyticsChartLegendEntry[] legendEntries: AnalyticsChartLegendEntry[] @@ -165,6 +167,7 @@ const { allChartDatasets: computed(() => props.allChartDatasets), chartRangeBounds: computed(() => props.chartRangeBounds), shouldShowPreviousPeriod: computed(() => props.shouldShowPreviousPeriod), + isSameDayLastWeekComparison: computed(() => props.isSameDayLastWeekComparison), onRangeSelected: (start, end, groupBy) => emit('range-select', start, end, groupBy), }) diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-interactions.ts b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-interactions.ts index 25c7099a43..8a51f2164c 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-interactions.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-interactions.ts @@ -27,6 +27,7 @@ export function useAnalyticsChartInteractions({ allChartDatasets, chartRangeBounds, shouldShowPreviousPeriod, + isSameDayLastWeekComparison, onRangeSelected, }: { isDataLoading: ComputedRef @@ -36,6 +37,7 @@ export function useAnalyticsChartInteractions({ allChartDatasets: ComputedRef chartRangeBounds: ComputedRef shouldShowPreviousPeriod: ComputedRef + isSameDayLastWeekComparison: ComputedRef onRangeSelected: (start: Date, end: Date, groupBy: AnalyticsGroupByPreset) => void }) { const chartContainer = ref(null) @@ -226,7 +228,9 @@ export function useAnalyticsChartInteractions({ const rangeBounds = chartRangeBounds.value if (!bucketRange || !rangeBounds) return null - const periodMs = rangeBounds.end.getTime() - rangeBounds.start.getTime() + const periodMs = isSameDayLastWeekComparison.value + ? 7 * 24 * 60 * 60 * 1000 + : rangeBounds.end.getTime() - rangeBounds.start.getTime() if (!Number.isFinite(periodMs) || periodMs <= 0) return null return { diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/index.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/index.vue index 2011888a65..c28cf9fb01 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/index.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/index.vue @@ -24,6 +24,7 @@ :show-top-graph-datasets-button="showTopGraphDatasetsButton" :can-use-ratio-mode="canUseRatioMode" :can-show-previous-period="canShowPreviousPeriodToggle" + :is-same-day-last-week-comparison="isSameDayLastWeekComparison" :has-chart-events="hasChartEvents" :has-project-events="hasProjectEvents" :small-toggles="!isMobileLayout" @@ -66,6 +67,7 @@ :fetch-request="fetchRequest" :slice-count="sliceCount" :should-show-previous-period="shouldShowPreviousPeriod" + :is-same-day-last-week-comparison="isSameDayLastWeekComparison" :all-chart-datasets="allChartDatasets" :current-legend-entries="currentLegendEntries" :legend-entries="legendEntries" @@ -130,6 +132,7 @@ const { isGraphDatasetSelectionActive, selectedProjectIds: currentSelectedProjectIds, selectedTimeframeMode, + selectedTimeframe, selectedCustomTimeframeStartDate, selectedCustomTimeframeEndDate, selectedGroupBy: selectedDashboardGroupBy, @@ -140,6 +143,9 @@ const { } = dashboardContext const isDataLoading = computed(() => isLoading.value) +const isSameDayLastWeekComparison = computed( + () => selectedTimeframeMode.value === 'preset' && selectedTimeframe.value === 'yesterday', +) const defaultShowProjectEvents = computed(() => getDefaultAnalyticsGraphProjectEventsVisibility(currentSelectedProjectIds.value), ) @@ -194,6 +200,7 @@ const { allChartDatasets, previousChartDatasets, shouldShowPreviousPeriod, + isSameDayLastWeekComparison, isRatioMode, hiddenGraphDatasetIds, selectedBreakdowns, diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts index 49a0b7ea27..f6dfef4c5a 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts @@ -291,6 +291,14 @@ export const analyticsStatCardMessages = defineMessages({ id: 'analytics.stat.previous-period-comparison-short', defaultMessage: 'vs prev.', }, + sameDayLastWeekComparison: { + id: 'analytics.stat.same-day-last-week-comparison', + defaultMessage: 'vs. same day last week', + }, + sameDayLastWeekComparisonShort: { + id: 'analytics.stat.same-day-last-week-comparison-short', + defaultMessage: 'vs. last wk.', + }, }) export const analyticsGroupByMessages = defineMessages({ @@ -619,6 +627,10 @@ export const analyticsChartMessages = defineMessages({ id: 'analytics.chart.controls.previous-period', defaultMessage: 'Previous period', }, + sameDayLastWeek: { + id: 'analytics.chart.controls.same-day-last-week', + defaultMessage: 'Same day last week', + }, ratio: { id: 'analytics.chart.controls.ratio', defaultMessage: 'Ratio', @@ -664,6 +676,14 @@ export const analyticsChartMessages = defineMessages({ id: 'analytics.chart.tooltip.previous-period-short', defaultMessage: '(prev.)', }, + sameDayLastWeekSuffix: { + id: 'analytics.chart.legend.same-day-last-week-suffix', + defaultMessage: '{name} (Last wk.)', + }, + sameDayLastWeekShort: { + id: 'analytics.chart.tooltip.same-day-last-week-short', + defaultMessage: '(last wk.)', + }, tooltipPinned: { id: 'analytics.chart.tooltip.pinned', defaultMessage: 'Chart tooltip pinned', diff --git a/apps/frontend/src/components/analytics-dashboard/stat-cards/StatCard.vue b/apps/frontend/src/components/analytics-dashboard/stat-cards/StatCard.vue index 071e2eb584..7376550ac3 100644 --- a/apps/frontend/src/components/analytics-dashboard/stat-cards/StatCard.vue +++ b/apps/frontend/src/components/analytics-dashboard/stat-cards/StatCard.vue @@ -54,7 +54,7 @@ @@ -117,6 +117,7 @@ const props = defineProps<{ statLabel: string statTooltip?: string vsPrevPeriodPercent: string | null + isSameDayLastWeekComparison?: boolean icon: string active?: boolean disabled?: boolean @@ -127,6 +128,16 @@ const emit = defineEmits<{ }>() const { formatMessage } = useVIntl() +const comparisonMessage = computed(() => + props.isSameDayLastWeekComparison + ? analyticsStatCardMessages.sameDayLastWeekComparison + : analyticsStatCardMessages.previousPeriodComparison, +) +const comparisonMessageShort = computed(() => + props.isSameDayLastWeekComparison + ? analyticsStatCardMessages.sameDayLastWeekComparisonShort + : analyticsStatCardMessages.previousPeriodComparisonShort, +) const statCardIconMap: Record = { clock: ClockIcon, timer: TimerIcon, diff --git a/apps/frontend/src/components/analytics-dashboard/stat-cards/StatCards.vue b/apps/frontend/src/components/analytics-dashboard/stat-cards/StatCards.vue index 601dfaf89e..df27b477d4 100644 --- a/apps/frontend/src/components/analytics-dashboard/stat-cards/StatCards.vue +++ b/apps/frontend/src/components/analytics-dashboard/stat-cards/StatCards.vue @@ -28,6 +28,7 @@ :stat-label="card.statLabel" :stat-tooltip="card.statTooltip" :vs-prev-period-percent="card.vsPrevPeriodPercent" + :is-same-day-last-week-comparison="isSameDayLastWeekComparison" :icon="card.icon" :active="activeStat === card.key" :disabled="card.disabled" @@ -61,6 +62,8 @@ const { percentChanges, hasPreviousPeriodComparison, selectedBreakdowns, + selectedTimeframeMode, + selectedTimeframe, isAnalyticsDashboardStatRelevant, } = injectAnalyticsDashboardContext() const formatNumber = useFormatNumber() @@ -70,6 +73,9 @@ const showMonetizationBanner = computed( () => selectedBreakdowns.value.includes('monetization') && !monetizationBannerDismissed.value, ) const MAX_PREVIOUS_PERIOD_PERCENT_DISPLAY = 1000 +const isSameDayLastWeekComparison = computed( + () => selectedTimeframeMode.value === 'preset' && selectedTimeframe.value === 'yesterday', +) const compactNumberFormatter = computed( () => diff --git a/apps/frontend/src/providers/analytics/analytics-data-utils.ts b/apps/frontend/src/providers/analytics/analytics-data-utils.ts index d415eb8a57..2deebf6035 100644 --- a/apps/frontend/src/providers/analytics/analytics-data-utils.ts +++ b/apps/frontend/src/providers/analytics/analytics-data-utils.ts @@ -37,6 +37,7 @@ function isProjectAnalyticsPoint( export function buildComparisonFetchRequest( fetchRequest: Labrinth.Analytics.v3.FetchRequest | null, minStartTime = ANALYTICS_START_TIME, + comparisonOffsetMs?: number, ): AnalyticsProjectFetchRequest | null { if (!isAnalyticsFetchRequestReady(fetchRequest)) { return null @@ -50,7 +51,12 @@ export function buildComparisonFetchRequest( return null } - const previousStart = new Date(startTimestamp - duration) + const offset = comparisonOffsetMs ?? duration + if (!Number.isFinite(offset) || offset < duration) { + return null + } + + const previousStart = new Date(startTimestamp - offset) if (previousStart.getTime() < minStartTime) { return null @@ -64,7 +70,9 @@ export function buildComparisonFetchRequest( resolution: 'slices' in fetchRequest.time_range.resolution ? { - slices: fetchRequest.time_range.resolution.slices * 2, + slices: Math.ceil( + fetchRequest.time_range.resolution.slices * ((offset + duration) / duration), + ), } : fetchRequest.time_range.resolution, }, @@ -99,10 +107,11 @@ export function splitAnalyticsTimeSlices( timeSlices: Labrinth.Analytics.v3.TimeSlice[], fetchRequest: Labrinth.Analytics.v3.FetchRequest | null, minStartTime = ANALYTICS_START_TIME, + comparisonOffsetMs?: number, ): AnalyticsTimeSliceSplit { if ( !isAnalyticsFetchRequestReady(fetchRequest) || - !buildComparisonFetchRequest(fetchRequest, minStartTime) + !buildComparisonFetchRequest(fetchRequest, minStartTime, comparisonOffsetMs) ) { return { currentTimeSlices: timeSlices, @@ -112,11 +121,20 @@ export function splitAnalyticsTimeSlices( const currentSliceCount = getAnalyticsTimeSliceCount(fetchRequest.time_range, timeSlices.length) const currentStartIndex = Math.max(0, timeSlices.length - currentSliceCount) - const previousStartIndex = Math.max(0, currentStartIndex - currentSliceCount) + const startTimestamp = new Date(fetchRequest.time_range.start).getTime() + const endTimestamp = new Date(fetchRequest.time_range.end).getTime() + const duration = endTimestamp - startTimestamp + const offsetSliceCount = Math.round( + currentSliceCount * ((comparisonOffsetMs ?? duration) / duration), + ) + const previousStartIndex = Math.max(0, currentStartIndex - offsetSliceCount) return { currentTimeSlices: timeSlices.slice(currentStartIndex), - previousTimeSlices: timeSlices.slice(previousStartIndex, currentStartIndex), + previousTimeSlices: timeSlices.slice( + previousStartIndex, + previousStartIndex + currentSliceCount, + ), } } diff --git a/apps/frontend/src/providers/analytics/analytics.ts b/apps/frontend/src/providers/analytics/analytics.ts index a623a3913e..e02d929871 100644 --- a/apps/frontend/src/providers/analytics/analytics.ts +++ b/apps/frontend/src/providers/analytics/analytics.ts @@ -1031,8 +1031,17 @@ export function createAnalyticsDashboardContext( return ANALYTICS_START_TIME }) + const comparisonOffsetMs = computed(() => + selectedTimeframeMode.value === 'preset' && selectedTimeframe.value === 'yesterday' + ? 7 * 24 * 60 * 60 * 1000 // 7 days + : undefined, + ) const comparisonFetchRequest = computed(() => - buildComparisonFetchRequest(fetchRequest.value, analyticsComparisonStartTime.value), + buildComparisonFetchRequest( + fetchRequest.value, + analyticsComparisonStartTime.value, + comparisonOffsetMs.value, + ), ) const analyticsTimeSlicesFetchRequest = computed( () => comparisonFetchRequest.value ?? fetchRequest.value, @@ -1126,8 +1135,11 @@ export function createAnalyticsDashboardContext( const dailyFetchRequest = buildDailyAnalyticsFetchRequest(fetchRequest.value) return ( - buildComparisonFetchRequest(dailyFetchRequest, analyticsComparisonStartTime.value) ?? - dailyFetchRequest + buildComparisonFetchRequest( + dailyFetchRequest, + analyticsComparisonStartTime.value, + comparisonOffsetMs.value, + ) ?? dailyFetchRequest ) }) @@ -1583,6 +1595,7 @@ export function createAnalyticsDashboardContext( nextAnalyticsData.metrics, fetchRequest.value, analyticsComparisonStartTime.value, + comparisonOffsetMs.value, ) timeSlices.value = splitTimeSlices.currentTimeSlices previousTimeSlices.value = splitTimeSlices.previousTimeSlices