aboutsummaryrefslogtreecommitdiff
path: root/web/frontend/analytics-logic.ts
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-25 13:25:35 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-25 13:25:35 +0200
commitd5de3a6fefbc57bccfb1fe688f9c190ccf87d58d (patch)
tree4a368e1331e04348353a9d65b94a07f4e94ee6bf /web/frontend/analytics-logic.ts
parent87e29e1eb5a113fbb40d452a193219a40780c302 (diff)
analytics grouping
Diffstat (limited to 'web/frontend/analytics-logic.ts')
-rw-r--r--web/frontend/analytics-logic.ts112
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) }));
+};