Compare commits

...
12 changed files with 120 additions and 17 deletions
@@ -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"
>
<div class="flex items-center justify-between gap-3 px-3 py-2.5 text-xs font-medium">
<span class="font-semibold text-primary">{{ activeControlCountLabel }}</span>
@@ -68,7 +68,7 @@
>
<HistoryIcon class="size-4 shrink-0 text-secondary" aria-hidden="true" />
<span class="min-w-0 truncate">
{{ formatMessage(analyticsChartMessages.previousPeriod) }}
{{ formatMessage(comparisonMessage) }}
</span>
</label>
<Toggle
@@ -172,6 +172,7 @@ const props = defineProps<{
showPreviousPeriod: boolean
canUseRatioMode: boolean
canShowPreviousPeriod: boolean
isSameDayLastWeekComparison: boolean
hasChartEvents: boolean
hasProjectEvents: boolean
smallToggles: boolean
@@ -201,6 +202,11 @@ const previousPeriodToggleId = useId()
const modrinthEventsToggleId = useId()
const projectEventsToggleId = useId()
const { formatMessage } = useVIntl()
const comparisonMessage = computed(() =>
props.isSameDayLastWeekComparison
? analyticsChartMessages.sameDayLastWeek
: analyticsChartMessages.previousPeriod,
)
const ratioModeModel = computed({
get: () => props.ratioMode,
@@ -40,6 +40,7 @@
v-model:show-previous-period="showPreviousPeriod"
:can-use-ratio-mode="canUseRatioMode"
:can-show-previous-period="canShowPreviousPeriod"
:is-same-day-last-week-comparison="isSameDayLastWeekComparison"
:has-chart-events="hasChartEvents"
:has-project-events="hasProjectEvents"
:small-toggles="smallToggles"
@@ -89,6 +90,7 @@ const props = defineProps<{
showTopGraphDatasetsButton: boolean
canUseRatioMode: boolean
canShowPreviousPeriod: boolean
isSameDayLastWeekComparison: boolean
hasChartEvents: boolean
hasProjectEvents: boolean
smallToggles: boolean
@@ -28,6 +28,7 @@ export function useAnalyticsChartLegend({
allChartDatasets,
previousChartDatasets,
shouldShowPreviousPeriod,
isSameDayLastWeekComparison,
isRatioMode,
hiddenGraphDatasetIds,
selectedBreakdowns,
@@ -40,6 +41,7 @@ export function useAnalyticsChartLegend({
allChartDatasets: ComputedRef<ChartDataset[]>
previousChartDatasets: ComputedRef<ChartDataset[]>
shouldShowPreviousPeriod: ComputedRef<boolean>
isSameDayLastWeekComparison: ComputedRef<boolean>
isRatioMode: Ref<boolean>
hiddenGraphDatasetIds: Ref<string[]>
selectedBreakdowns: Ref<readonly AnalyticsBreakdownPreset[]>
@@ -51,6 +53,11 @@ export function useAnalyticsChartLegend({
const { formatMessage } = useVIntl()
const hoveredLegendEntryId = ref<string | null>(null)
const hiddenDatasetIds = computed(() => new Set(hiddenGraphDatasetIds.value))
const previousPeriodSuffixMessage = computed(() =>
isSameDayLastWeekComparison.value
? analyticsChartMessages.sameDayLastWeekSuffix
: analyticsChartMessages.previousPeriodSuffix,
)
const previousChartDatasetByOriginalId = computed(() => {
const datasets = new Map<string, ChartDataset>()
for (const dataset of previousChartDatasets.value) {
@@ -117,7 +124,7 @@ export function useAnalyticsChartLegend({
const previousDataset = previousChartDatasetByOriginalId.value.get(entry.id)
const previousEntry: AnalyticsChartLegendEntry = {
id: getPreviousPeriodDatasetId(entry.id),
name: formatMessage(analyticsChartMessages.previousPeriodSuffix, { name: entry.name }),
name: formatMessage(previousPeriodSuffixMessage.value, { name: entry.name }),
projectName: entry.projectName,
tooltip: entry.tooltip,
color: entry.color,
@@ -151,7 +158,7 @@ export function useAnalyticsChartLegend({
)
datasets.set(getPreviousPeriodDatasetId(dataset.projectId), {
projectId: getPreviousPeriodDatasetId(dataset.projectId),
label: formatMessage(analyticsChartMessages.previousPeriodSuffix, {
label: formatMessage(previousPeriodSuffixMessage.value, {
name: dataset.label,
}),
projectName: dataset.projectName,
@@ -21,7 +21,7 @@
<span v-if="previousRangeLabel" class="min-w-0 space-x-1 truncate text-xs text-primary">
<span class="font-medium">{{ previousRangeLabel }}</span>
<span class="font-normal text-secondary">
{{ formatMessage(analyticsChartMessages.previousPeriodShort) }}
{{ formatMessage(previousPeriodMessage) }}
</span>
</span>
</div>
@@ -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
@@ -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),
})
@@ -27,6 +27,7 @@ export function useAnalyticsChartInteractions({
allChartDatasets,
chartRangeBounds,
shouldShowPreviousPeriod,
isSameDayLastWeekComparison,
onRangeSelected,
}: {
isDataLoading: ComputedRef<boolean>
@@ -36,6 +37,7 @@ export function useAnalyticsChartInteractions({
allChartDatasets: ComputedRef<ChartDataset[]>
chartRangeBounds: ComputedRef<AnalyticsChartRangeBounds | null>
shouldShowPreviousPeriod: ComputedRef<boolean>
isSameDayLastWeekComparison: ComputedRef<boolean>
onRangeSelected: (start: Date, end: Date, groupBy: AnalyticsGroupByPreset) => void
}) {
const chartContainer = ref<HTMLElement | null>(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 {
@@ -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,
@@ -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',
@@ -54,7 +54,7 @@
</span>
</template>
<template v-else>
<div v-if="vsPrevPeriodPercent" class="flex items-center gap-1 text-sm">
<div v-if="vsPrevPeriodPercent" class="flex flex-wrap items-center gap-x-1 text-sm">
<span
class="inline-flex items-center gap-1 font-semibold"
:class="{
@@ -79,7 +79,7 @@
'text-primary': !disabled,
}"
>
{{ formatMessage(analyticsStatCardMessages.previousPeriodComparison) }}
{{ formatMessage(comparisonMessage) }}
</span>
<span
class="visible mt-px text-xs sm:hidden"
@@ -88,7 +88,7 @@
'text-primary': !disabled,
}"
>
{{ formatMessage(analyticsStatCardMessages.previousPeriodComparisonShort) }}
{{ formatMessage(comparisonMessageShort) }}
</span>
</div>
</template>
@@ -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<string, IconComponent> = {
clock: ClockIcon,
timer: TimerIcon,
@@ -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(
() =>
@@ -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,
),
}
}
@@ -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