diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue
index c5c4d3112..6dd495c9c 100644
--- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue
+++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue
@@ -25,7 +25,7 @@
class="inline-flex items-center"
>
+
+
+
+
+
+ {{ label }}
+
+
+
+
+
+
+
+
diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts
index 1f5f414d0..21cae3cd7 100644
--- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts
+++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts
@@ -23,7 +23,6 @@ type BuildAnalyticsTableColumnsOptions = {
selectedBreakdowns: readonly AnalyticsTableBreakdownPreset[]
selectedFilters: AnalyticsSelectedFilters
showBreakdownColumn: boolean
- showProjectVersionProjectColumn: boolean
formatMessage: FormatMessage
getRelevantAnalyticsDashboardStats: (
breakdowns: readonly AnalyticsBreakdownPreset[],
@@ -43,7 +42,6 @@ export function buildAnalyticsTableColumns({
selectedBreakdowns,
selectedFilters,
showBreakdownColumn,
- showProjectVersionProjectColumn,
formatMessage,
getRelevantAnalyticsDashboardStats,
}: BuildAnalyticsTableColumnsOptions): TableColumn[] {
@@ -66,20 +64,11 @@ export function buildAnalyticsTableColumns({
key: getAnalyticsTableBreakdownColumnKey(breakdown),
label: getAnalyticsTableBreakdownColumnLabel(breakdown, formatMessage),
enableSorting: true,
- width: breakdown === 'project' ? '25%' : undefined,
+ width: breakdown === 'project' && selectedBreakdowns.length === 1 ? '45%' : undefined,
})
}
}
- if (showProjectVersionProjectColumn) {
- nextColumns.push({
- key: 'project',
- label: formatAnalyticsBreakdownLabel('project', formatMessage),
- enableSorting: true,
- width: '25%',
- })
- }
-
for (const stat of stats) {
const column = getAnalyticsTableMetricColumn(stat, formatMessage)
if (column) {
@@ -102,6 +91,7 @@ export function getAnalyticsTableMetricColumn(
enableSorting: true,
defaultSortDirection: 'desc',
align: 'right',
+ width: '20%',
}
case 'downloads':
return {
@@ -110,6 +100,7 @@ export function getAnalyticsTableMetricColumn(
enableSorting: true,
defaultSortDirection: 'desc',
align: 'right',
+ width: '20%',
}
case 'revenue':
return {
@@ -118,6 +109,7 @@ export function getAnalyticsTableMetricColumn(
enableSorting: true,
defaultSortDirection: 'desc',
align: 'right',
+ width: '20%',
}
case 'playtime':
return {
@@ -126,6 +118,7 @@ export function getAnalyticsTableMetricColumn(
enableSorting: true,
defaultSortDirection: 'desc',
align: 'right',
+ width: '20%',
}
default:
return null
diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-csv-export.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-csv-export.ts
index ee0cab3ef..1bd6f21ef 100644
--- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-csv-export.ts
+++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-csv-export.ts
@@ -67,6 +67,8 @@ function getAnalyticsTableCsvCellValue(
return row.date
case 'project':
return row.project
+ case 'dependent_on':
+ return row.dependent_on
case 'breakdown':
return row.breakdownDisplay
case 'views':
diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts
index 04c7d0a12..a4fe46007 100644
--- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts
+++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts
@@ -1,8 +1,11 @@
import type { Labrinth } from '@modrinth/api-client'
-import type {
- AnalyticsBreakdownPreset,
- AnalyticsDashboardStat,
+import {
+ type AnalyticsBreakdownPreset,
+ type AnalyticsDashboardStat,
+ type AnalyticsSelectedFilters,
+ doesAnalyticsPointMatchNormalizedFilters,
+ normalizeAnalyticsSelectedFilters,
} from '~/providers/analytics/analytics'
import {
@@ -12,13 +15,18 @@ import {
getSliceCount,
} from '../analytics-chart/analytics-chart-utils'
import type { FormatMessage } from '../analytics-messages'
-import { analyticsMessages } from '../analytics-messages'
+import {
+ analyticsMessages,
+ formatAnalyticsDependentProjectFallbackLabel,
+} from '../analytics-messages'
import {
ALL_BREAKDOWN_VALUE,
COMBINED_BREAKDOWN_LABEL_SEPARATOR,
getAnalyticsBreakdownDatasetId,
getAnalyticsBreakdownKey,
getAnalyticsBreakdownValues,
+ isNoDependentAnalyticsBreakdownValue,
+ isUnknownAnalyticsBreakdownValue,
} from '../breakdown'
import { getAnalyticsTableBreakdownColumnKey } from './analytics-table-columns'
import type {
@@ -36,8 +44,12 @@ type BuildAnalyticsTableRowsOptions = {
timeSlices: Labrinth.Analytics.v3.TimeSlice[]
selectedBreakdowns: readonly AnalyticsTableBreakdownPreset[]
selectedProjectIds: ReadonlySet
+ selectedFilters: AnalyticsSelectedFilters
+ dependentProjectTypesById: ReadonlyMap
+ includeDependentProjectTooltipContext: boolean
relevantStats: ReadonlySet
projectNamesById: ReadonlyMap
+ userNamesById: ReadonlyMap
getVersionDisplayName: (versionId: string) => string
getVersionProjectName: (versionId: string) => string | undefined
showTimeInBucketLabel: boolean
@@ -51,8 +63,12 @@ export function buildAnalyticsTableRows({
timeSlices,
selectedBreakdowns,
selectedProjectIds,
+ selectedFilters,
+ dependentProjectTypesById,
+ includeDependentProjectTooltipContext,
relevantStats,
projectNamesById,
+ userNamesById,
getVersionDisplayName,
getVersionProjectName,
showTimeInBucketLabel,
@@ -72,6 +88,7 @@ export function buildAnalyticsTableRows({
const projectDisplayValues = new Map()
const nextRows = new Map()
const bucketLabelsBySliceIndex = new Map()
+ const normalizedFilters = normalizeAnalyticsSelectedFilters(selectedFilters)
function getBreakdownDisplayValue(
breakdownValue: string,
@@ -84,6 +101,7 @@ export function buildAnalyticsTableRows({
breakdownValue,
breakdown,
projectNamesById,
+ userNamesById,
getVersionDisplayName,
formatMessage,
)
@@ -111,6 +129,15 @@ export function buildAnalyticsTableRows({
return displayValue
}
+ function getProjectVersionIdForBreakdownValues(breakdownValues: readonly string[]) {
+ const versionBreakdownIndex = selectedBreakdowns.indexOf('version_id')
+ if (versionBreakdownIndex === -1) {
+ return ''
+ }
+
+ return breakdownValues[versionBreakdownIndex] ?? ''
+ }
+
function getBreakdownDisplays(breakdownValues: readonly string[]) {
const displays: AnalyticsTableBreakdownDisplayValues = {}
@@ -118,6 +145,19 @@ export function buildAnalyticsTableRows({
displays[breakdown] = getBreakdownDisplayValue(breakdownValues[index] ?? '', breakdown)
})
+ const dependentProjectBreakdownIndex = selectedBreakdowns.indexOf('dependent_project_download')
+ const downloadReasonBreakdownIndex = selectedBreakdowns.indexOf('download_reason')
+ if (
+ dependentProjectBreakdownIndex !== -1 &&
+ downloadReasonBreakdownIndex !== -1 &&
+ isUnknownAnalyticsBreakdownValue(breakdownValues[dependentProjectBreakdownIndex])
+ ) {
+ displays.dependent_project_download = formatAnalyticsDependentProjectFallbackLabel(
+ breakdownValues[downloadReasonBreakdownIndex],
+ formatMessage,
+ )
+ }
+
return displays
}
@@ -157,6 +197,7 @@ export function buildAnalyticsTableRows({
function createRow(
rowId: string,
breakdownValues: readonly string[],
+ dependentOnProjectId?: string,
bucketLabel?: { date: string; dateMs: number },
) {
const breakdownKey =
@@ -169,6 +210,12 @@ export function buildAnalyticsTableRows({
date: bucketLabel?.date ?? '',
dateMs: bucketLabel?.dateMs ?? 0,
project: getProjectDisplayValueForBreakdownValues(breakdownValues),
+ projectVersionId: getProjectVersionIdForBreakdownValues(breakdownValues),
+ dependent_on: dependentOnProjectId
+ ? (projectNamesById.get(dependentOnProjectId) ?? dependentOnProjectId)
+ : '',
+ dependentOnProjectId: dependentOnProjectId ?? '',
+ dependentOnProjectIds: dependentOnProjectId ? [dependentOnProjectId] : [],
breakdown: breakdownKey,
breakdownValues: Object.fromEntries(
selectedBreakdowns.map((breakdown, index) => [breakdown, breakdownValues[index] ?? '']),
@@ -190,6 +237,18 @@ export function buildAnalyticsTableRows({
return row
}
+ function addDependentOnProjectIdToRow(row: AnalyticsTableRow, projectId: string | undefined) {
+ if (!projectId || row.dependentOnProjectIds.includes(projectId)) {
+ return
+ }
+
+ row.dependentOnProjectIds.push(projectId)
+ if (!row.dependentOnProjectId) {
+ row.dependentOnProjectId = projectId
+ row.dependent_on = projectNamesById.get(projectId) ?? projectId
+ }
+ }
+
if (!includeDate && selectedBreakdowns.length === 0) {
createRow(ALL_PROJECTS_BREAKDOWN_VALUE, [])
}
@@ -211,6 +270,15 @@ export function buildAnalyticsTableRows({
if (!selectedProjectIds.has(point.source_project)) {
continue
}
+ if (
+ !doesAnalyticsPointMatchNormalizedFilters(
+ point,
+ normalizedFilters,
+ dependentProjectTypesById,
+ )
+ ) {
+ continue
+ }
const pointStat = getAnalyticsTableStatForMetric(point.metric_kind)
if (!pointStat || !relevantStats.has(pointStat)) {
@@ -226,12 +294,21 @@ export function buildAnalyticsTableRows({
}
const nextBucketLabel = includeDate ? (bucketLabel ?? getBucketLabel(sliceIndex)) : undefined
+ const dependentOnProjectId = includeDependentProjectTooltipContext
+ ? point.source_project
+ : undefined
+ const dependentTooltipProjectId = selectedBreakdowns.includes('dependent_project_download')
+ ? point.source_project
+ : undefined
const breakdownKey =
breakdownValues.length === 0
? ALL_PROJECTS_BREAKDOWN_VALUE
: getAnalyticsBreakdownKey(breakdownValues)
const rowId = includeDate ? `${nextBucketLabel?.dateMs ?? 0}::${breakdownKey}` : breakdownKey
- const row = nextRows.get(rowId) ?? createRow(rowId, breakdownValues, nextBucketLabel)
+ const row =
+ nextRows.get(rowId) ??
+ createRow(rowId, breakdownValues, dependentOnProjectId, nextBucketLabel)
+ addDependentOnProjectIdToRow(row, dependentTooltipProjectId)
addAnalyticsMetricToTableRow(row, point)
}
})
@@ -292,11 +369,21 @@ function formatAnalyticsTableBreakdownDisplayValue(
value: string,
breakdown: AnalyticsTableBreakdownPreset,
projectNamesById: ReadonlyMap,
+ userNamesById: ReadonlyMap,
getVersionDisplayName: (versionId: string) => string,
formatMessage: FormatMessage,
): string {
- if (breakdown === 'project') {
+ if (breakdown === 'project' || breakdown === 'dependent_project_download') {
+ if (breakdown === 'dependent_project_download') {
+ if (isNoDependentAnalyticsBreakdownValue(value)) {
+ return formatMessage(analyticsMessages.noDependent)
+ }
+ if (isUnknownAnalyticsBreakdownValue(value)) {
+ return formatMessage(analyticsMessages.unknown)
+ }
+ }
+
return projectNamesById.get(value) ?? value
}
- return formatBreakdownLabel(value, breakdown, getVersionDisplayName, formatMessage)
+ return formatBreakdownLabel(value, breakdown, getVersionDisplayName, userNamesById, formatMessage)
}
diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-search-filtering.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-search-filtering.ts
index 406c3c8c3..a70e24d5d 100644
--- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-search-filtering.ts
+++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-search-filtering.ts
@@ -3,7 +3,7 @@ import type { TableColumn } from '@modrinth/ui'
import { isAnalyticsTableBreakdownColumnKey } from './analytics-table-columns'
import type { AnalyticsTableColumnKey, AnalyticsTableRow } from './analytics-table-types'
-const SEARCHABLE_COLUMN_KEYS = new Set(['date', 'project'])
+const SEARCHABLE_COLUMN_KEYS = new Set(['date', 'project', 'dependent_on'])
export function getAnalyticsTableSearchableColumns(
columns: TableColumn[],
@@ -39,6 +39,8 @@ function getAnalyticsTableSearchableCellValue(
return row.date
case 'project':
return row.project
+ case 'dependent_on':
+ return row.dependent_on
case 'breakdown':
return row.breakdownDisplay
default:
diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-sorting.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-sorting.ts
index d55f8ef26..0b47d7acc 100644
--- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-sorting.ts
+++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-sorting.ts
@@ -129,6 +129,15 @@ function getAnalyticsTableRowComparator(
directionFactor,
sortCollator,
)
+ case 'dependent_on':
+ return (left, right) =>
+ compareAnalyticsTableRows(
+ left,
+ right,
+ sortCollator.compare(left.dependent_on, right.dependent_on),
+ directionFactor,
+ sortCollator,
+ )
case 'breakdown':
return (left, right) =>
compareAnalyticsTableRows(
diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-types.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-types.ts
index da343e723..167b5996f 100644
--- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-types.ts
+++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-types.ts
@@ -18,11 +18,15 @@ export type AnalyticsTableSortState = {
export type AnalyticsTableSortDirectionValue = AnalyticsTableSortDirection
export type AnalyticsTableRow = {
- [key: string]: string | number | AnalyticsTableBreakdownDisplayValues
+ [key: string]: string | number | string[] | AnalyticsTableBreakdownDisplayValues
id: string
date: string
dateMs: number
project: string
+ projectVersionId: string
+ dependent_on: string
+ dependentOnProjectId: string
+ dependentOnProjectIds: string[]
breakdown: string
breakdownValues: AnalyticsTableBreakdownDisplayValues
breakdownDisplays: AnalyticsTableBreakdownDisplayValues
diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/index.vue b/apps/frontend/src/components/analytics-dashboard/analytics-table/index.vue
index ac0dcdb1f..8f82206bb 100644
--- a/apps/frontend/src/components/analytics-dashboard/analytics-table/index.vue
+++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/index.vue
@@ -55,32 +55,92 @@
{{ value }}
-
- {{ value }}
+
+
- {{ value }}
+ {{ value }}
- {{ value }}
+ {{ value }}
- {{ value }}
+ {{ value }}
- {{ value }}
+ {{ value }}
-
- {{ value }}
+
+
+
+
+
+
+
+ {{ value }}
+
+
+
+
+
+
+
+
+ {{ value }}
+
- {{ value }}
+ {{ value }}
- {{ value }}
-
-
- {{ value }}
+ {{ value }}
{{ formatInteger(row.views) }}
@@ -130,7 +190,7 @@