diff options
Diffstat (limited to 'web/frontend/analytics-logic.ts')
| -rw-r--r-- | web/frontend/analytics-logic.ts | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/web/frontend/analytics-logic.ts b/web/frontend/analytics-logic.ts index e8a1559..5da8b80 100644 --- a/web/frontend/analytics-logic.ts +++ b/web/frontend/analytics-logic.ts @@ -1,3 +1,5 @@ +import type { AnalyticsPoint } from "./types.js"; + export const niceStep = (range: number, target: number): number => { if (range <= 0) return 1; const ideal = range / target; @@ -41,3 +43,113 @@ export const formatHours = (hours: number): string => { if (m === 0) return `${h}h`; return `${h}h${String(m).padStart(2, "0")}`; }; + +export type ColorBy = "none" | "year" | "rideType" | "bikeType"; + +export interface RideGroup { + label: string; + count: number; + colorIndex: number; +} + +export interface GroupedPoint { + point: AnalyticsPoint; + colorIndex: number; +} + +const UNKNOWN_LABEL = "Unknown"; + +const rideTypeLabels: Record<string, string> = { + "": UNKNOWN_LABEL, + training: "Training", + indoor: "Indoor", + race: "Race", + transport: "Transport", +}; + +const bikeTypeLabels: Record<string, string> = { + "": UNKNOWN_LABEL, + road: "Road", + mtb: "MTB", +}; + +const rideTypeOrder = ["training", "indoor", "race", "transport"]; +const bikeTypeOrder = ["road", "mtb"]; + +export const analyticsRideTypeLabel = (value: string): string => rideTypeLabels[value] ?? value; +export const analyticsBikeTypeLabel = (value: string): string => bikeTypeLabels[value] ?? value; + +const rideTypeColorIndex = (value: string): number => { + const index = rideTypeOrder.indexOf(value); + return index < 0 ? rideTypeOrder.length : index; +}; + +const bikeTypeColorIndex = (value: string): number => { + const index = bikeTypeOrder.indexOf(value); + return index < 0 ? bikeTypeOrder.length : index; +}; + +const yearColorIndex = (value: number, years: number[]): number => years.indexOf(value); + +export const groupRidePoints = (points: AnalyticsPoint[], colorBy: ColorBy): RideGroup[] => { + if (colorBy === "none") return []; + const counts = new Map<string, number>(); + for (const point of points) { + const key = groupKey(colorBy, point); + counts.set(key, (counts.get(key) ?? 0) + 1); + } + return groupKeys(colorBy, counts).map((key, index) => ({ + label: groupLabel(colorBy, key), + count: counts.get(key) ?? 0, + colorIndex: colorIndexForGroupKey(colorBy, key, index), + })); +}; + +const colorIndexForGroupKey = (colorBy: ColorBy, key: string, compactIndex: number): number => { + if (colorBy === "rideType") return rideTypeColorIndex(key); + if (colorBy === "bikeType") return bikeTypeColorIndex(key); + return compactIndex; +}; + +const groupKey = (colorBy: ColorBy, point: AnalyticsPoint): string => { + if (colorBy === "year") return String(point.year); + if (colorBy === "rideType") return point.rideType; + if (colorBy === "bikeType") return point.bikeType; + return ""; +}; + +const groupLabel = (colorBy: ColorBy, key: string): string => { + if (colorBy === "year") return key; + if (colorBy === "rideType") return analyticsRideTypeLabel(key); + if (colorBy === "bikeType") return analyticsBikeTypeLabel(key); + return key; +}; + +const groupKeys = (colorBy: ColorBy, counts: Map<string, number>): string[] => { + const keys = [...counts.keys()]; + if (colorBy === "rideType") { + return [...rideTypeOrder.filter((key) => counts.has(key)), ...(counts.has("") ? [""] : [])]; + } + if (colorBy === "bikeType") { + return [...bikeTypeOrder.filter((key) => counts.has(key)), ...(counts.has("") ? [""] : [])]; + } + if (colorBy === "year") { + return keys.sort((a, b) => Number(a) - Number(b)); + } + return keys; +}; + +export const colorIndexForPoint = (point: AnalyticsPoint, colorBy: ColorBy): number => { + if (colorBy === "year") return 0; + if (colorBy === "rideType") return rideTypeColorIndex(point.rideType); + if (colorBy === "bikeType") return bikeTypeColorIndex(point.bikeType); + return 0; +}; + +export const groupedPoints = (points: AnalyticsPoint[], colorBy: ColorBy): GroupedPoint[] => { + if (colorBy === "year") { + const years = [...new Set(points.map((point) => point.year))].sort((a, b) => a - b); + return points.map((point) => ({ point, colorIndex: yearColorIndex(point.year, years) })); + } + return points.map((point) => ({ point, colorIndex: colorIndexForPoint(point, colorBy) })); +}; |
