From 29cf2301c137a2cb715d16ef3450dd115ae3bbf5 Mon Sep 17 00:00:00 2001 From: tobinio Date: Thu, 03 Oct 2024 16:51:31 +0000 Subject: [PATCH] simple over time view - #5 --- utils/date.ts | 5 +++++ app/composables/useGameVersions.ts | 15 +++++++++++++++ app/composables/useVersionRange.ts | 16 ++-------------- app/pages/charts.vue | 27 +++++++++++++++++++++------ server/api/stats.ts | 9 ++------- server/api/statsTime.ts | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ server/utils/api/stats.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 file(s) changed, 150 insertion(s)(+), 27 deletion(s)(-) diff --git a/utils/date.ts b/utils/date.ts --- a/utils/date.ts +++ b/utils/date.ts @@ -1,3 +1,8 @@ export function dateToKey(date: Date): string { return `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}`; } + +export function keyToDate(key: string): Date { + const [year, month, day] = key.split("-").map(Number); + return new Date(Date.UTC(year as number, (month as number) - 1, day)); +} diff --git a/app/composables/useGameVersions.ts b/app/composables/useGameVersions.ts new file mode 100644 --- /dev/null +++ b/app/composables/useGameVersions.ts @@ -0,0 +1,15 @@ +export function useGameVersions(versionGroup: Ref) { + const { data } = useFetch("/api/gameVersions", { + query: { mode: versionGroup }, + }); + + return computed(() => { + if (!data.value) { + return []; + } + + return data.value.map((value) => { + return value.name; + }); + }); +} diff --git a/app/composables/useVersionRange.ts b/app/composables/useVersionRange.ts --- a/app/composables/useVersionRange.ts +++ b/app/composables/useVersionRange.ts @@ -1,23 +1,11 @@ -import { useSettings } from "~/composables/useSettings"; +import {useGameVersions} from "~/composables/useGameVersions"; export function useVersionRange( versionGroup: Ref, versionTo: Ref, versionFrom: Ref, ) { - const { data } = useFetch("/api/gameVersions", { - query: { mode: versionGroup }, - }); - - const versionNames = computed(() => { - if (!data.value) { - return []; - } - - return data.value.map((value) => { - return value.name; - }); - }); + const versionNames = useGameVersions(versionGroup) const to = computed(() => { if (!versionFrom.value) { diff --git a/app/pages/charts.vue b/app/pages/charts.vue --- a/app/pages/charts.vue +++ b/app/pages/charts.vue @@ -4,20 +4,30 @@ import { useExplanations } from "~/composables/useExplanations"; import { useFilterItem } from "~/composables/useFilterItem"; import { useVersionRange } from "#imports"; +import { useGameVersions } from "~/composables/useGameVersions"; const projectType = useFilterItem("projectType", "mod"); const stat = useFilterItem("stat", "count"); const versionGroup = useFilterItem("versionGroup", "major"); -const versionFrom = useFilterItem("versionFrom", undefined); -const versionTo = useFilterItem("versionFrom", undefined); -const { to, from } = useVersionRange(versionGroup, versionTo, versionFrom); +const time = useFilterItem("time", "current"); const exclusive = useFilterItem("exclusive", "yes"); const exclusiveBool = computed(() => exclusive.value === "yes"); -const { data } = useFetch("/api/stats", { +const versionFrom = useFilterItem("versionFrom", undefined); +const versionTo = useFilterItem("versionFrom", undefined); +const { to, from } = useVersionRange(versionGroup, versionTo, versionFrom); + +const gameVersions = useGameVersions(versionGroup); +const version = useFilterItem("version", gameVersions.value[0]); + +const url = computed(() => { + return time.value === "current" ? "/api/stats" : "/api/statsTime"; +}); + +const { data } = useFetch(url, { query: { mode: versionGroup, exclusive: exclusiveBool, @@ -25,6 +35,7 @@ stat: stat, versionTo: versionTo, versionFrom: versionFrom, + version: version, }, }); @@ -39,9 +50,13 @@ - - + + + + + + diff --git a/server/api/stats.ts b/server/api/stats.ts --- a/server/api/stats.ts +++ b/server/api/stats.ts @@ -5,7 +5,6 @@ mode: string; type: ProjectTypes; exclusive: string; - date: string | null; versionFrom: string | null; versionTo: string | null; }; @@ -13,12 +12,8 @@ export default defineEventHandler(async (event): Promise => { const query = getQuery(event); - let dateKey: string | null = query.date as string; - - if (!dateKey) { - const storage = useStorage("statistics"); - dateKey = await storage.getItem("latestDate"); - } + const storage = useStorage("statistics"); + const dateKey = await storage.getItem("latestDate"); if (!dateKey) { consola.error("no latestDate set"); diff --git a/server/api/statsTime.ts b/server/api/statsTime.ts new file mode 100644 --- /dev/null +++ b/server/api/statsTime.ts @@ -0,0 +1,52 @@ +import consola from "consola"; +import { exportStatsOverTime } from "~~/server/utils/api/stats"; + +type QueryData = { + stat: "versions" | "count" | "downloads"; + mode: string; + type: ProjectTypes; + exclusive: string; + version: string; +}; + +export default defineEventHandler(async (event): Promise => { + const query = getQuery(event); + + const storage = useStorage("statistics"); + const dateKey = await storage.getItem("latestDate"); + + if (!dateKey) { + consola.error("no latestDate set"); + + return { + labels: [], + data: [], + }; + } + + let typeFn: (value: StatsValue) => number; + + switch (query.stat) { + case "versions": { + typeFn = (value) => value.versions; + break; + } + case "count": { + typeFn = (value) => value.count; + break; + } + default: { + typeFn = (value) => value.downloads; + break; + } + } + + return exportStatsOverTime( + query.mode, + query.type, + query.exclusive === "true", + dateKey, + query.version, + typeFn, + ); +}); diff --git a/server/utils/api/stats.ts b/server/utils/api/stats.ts --- a/server/utils/api/stats.ts +++ b/server/utils/api/stats.ts @@ -1,4 +1,5 @@ import consola from "consola"; +import { dateToKey, keyToDate } from "~~/utils/date"; export type StatExport = { labels: string[]; @@ -26,6 +27,7 @@ return key; } +// maps database data to a format usable by the frontend function mapStats(stats: Stats, fn: (value: StatsValue) => number) { return { labels: stats.versions, @@ -41,6 +43,7 @@ }; } +// extract only versions in the given version range function filterVersions( mappedStats: StatExport, versionTo: string | null, @@ -90,4 +93,54 @@ filterVersions(mappedStats, versionTo, versionFrom); return mappedStats; +} + +export async function exportStatsOverTime( + versionCategory: string, + type: ProjectTypes, + exclusive: boolean, + firstDateKey: string, + version: string, + fn: (value: StatsValue) => number, +): Promise { + const storage = useStorage("statistics"); + + const statsOverTime: Stats = { + versions: [], + data: [], + }; + + const date = keyToDate(firstDateKey); + + while (true) { + const dateKey = dateToKey(date); + const key = getStorageKey(type, versionCategory, exclusive, dateKey); + const stats = await storage.getItem(key); + + if (!stats) { + break; + } + + const index = stats.versions.indexOf(version); + + statsOverTime.versions.splice(0, 0,dateKey); + + outer: for (const data of stats.data) { + for (const dataOverTime of statsOverTime.data) { + if (dataOverTime.name === data.name) { + dataOverTime.values.splice(0, 0, data.values[index]); + continue outer; + } + } + + statsOverTime.data.push({ + name: data.name, + values: [data.values[index]], + }); + } + + date.setUTCDate(date.getUTCDate() - 1); + } + + return mapStats(statsOverTime, fn); } -- tangled.sh