aboutsummaryrefslogtreecommitdiff
path: root/web/frontend/analytics.ts
diff options
context:
space:
mode:
Diffstat (limited to 'web/frontend/analytics.ts')
-rw-r--r--web/frontend/analytics.ts69
1 files changed, 65 insertions, 4 deletions
diff --git a/web/frontend/analytics.ts b/web/frontend/analytics.ts
index ef2479d..8c7435f 100644
--- a/web/frontend/analytics.ts
+++ b/web/frontend/analytics.ts
@@ -1,4 +1,4 @@
-import { formatCotacol, formatHours, formatSpeed, ticks } from "./analytics-logic.js";
+import { formatCotacol, formatHours, formatSpeed, groupRidePoints, groupedPoints, ticks, type ColorBy } from "./analytics-logic.js";
import type { AnalyticsPoint } from "./types.js";
interface Plot {
@@ -15,11 +15,13 @@ interface AnalyticsColors {
forest: string;
accent: string;
mutedText: string;
+ categories: string[];
}
const POINT_RADIUS = 4;
const HOVER_RADIUS = 7;
const HIT_RADIUS = 12;
+const CATEGORY_COLOR_COUNT = 8;
interface ScatterOptions {
outputId: string;
@@ -39,6 +41,8 @@ export class AnalyticsScatter {
private readonly xUnit: string;
private plot: Plot | null;
private hoveredIndex: number;
+ private colorBy: ColorBy;
+ private pointColorIndices: number[];
private readonly minX: number;
private readonly maxX: number;
private readonly minCotacol: number;
@@ -57,6 +61,8 @@ export class AnalyticsScatter {
this.context = context;
this.plot = null;
this.hoveredIndex = -1;
+ this.colorBy = "none";
+ this.pointColorIndices = points.map(() => 0);
let maxX = 0;
let maxCotacol = 0;
for (const point of points) {
@@ -74,6 +80,12 @@ export class AnalyticsScatter {
this.draw();
}
+ setColorBy(colorBy: ColorBy): void {
+ this.colorBy = colorBy;
+ this.pointColorIndices = groupedPoints(this.points, colorBy).map(({ colorIndex }) => colorIndex);
+ this.draw();
+ }
+
private draw(): void {
const rect = this.canvas.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
@@ -129,7 +141,7 @@ export class AnalyticsScatter {
private drawPoints(plot: Plot): void {
for (let index = 0; index < this.points.length; index++) {
const point = this.points[index];
- this.context.fillStyle = this.colors.accent;
+ this.context.fillStyle = this.pointColor(index);
this.context.beginPath();
this.context.arc(
this.xForValue(this.xAccessor(point), plot),
@@ -142,6 +154,12 @@ export class AnalyticsScatter {
}
}
+ private pointColor(index: number): string {
+ if (this.colorBy === "none" || this.colors.categories.length === 0) return this.colors.accent;
+ const colorIndex = this.pointColorIndices[index];
+ return this.colors.categories[colorIndex % this.colors.categories.length];
+ }
+
private drawHover(plot: Plot): void {
const point = this.points[this.hoveredIndex];
const x = this.xForValue(this.xAccessor(point), plot);
@@ -214,6 +232,28 @@ export class AnalyticsScatter {
}
}
+const renderLegend = (points: AnalyticsPoint[], colorBy: ColorBy, categories: string[]): void => {
+ const legend = document.getElementById("analytics-legend");
+ if (!legend) return;
+ legend.replaceChildren();
+ if (colorBy === "none") {
+ legend.hidden = true;
+ return;
+ }
+ for (const group of groupRidePoints(points, colorBy)) {
+ const item = document.createElement("li");
+ item.className = "analytics-legend-item";
+ const swatch = document.createElement("span");
+ swatch.className = "analytics-legend-swatch";
+ swatch.style.backgroundColor = categories.length > 0 ? categories[group.colorIndex % categories.length] : "currentColor";
+ const label = document.createElement("span");
+ label.textContent = `${group.label} ยท ${group.count}`;
+ item.append(swatch, label);
+ legend.append(item);
+ }
+ legend.hidden = false;
+};
+
export const mountAnalytics = (): void => {
const pointsScript = document.getElementById("analytics-points");
if (!pointsScript) return;
@@ -226,6 +266,11 @@ export const mountAnalytics = (): void => {
colorProbe.style.color = `var(${name})`;
return getComputedStyle(colorProbe).color;
};
+ const categories: string[] = [];
+ for (let index = 1; index <= CATEGORY_COLOR_COUNT; index++) {
+ const value = resolveColor(`--color-category-${index}`);
+ if (value) categories.push(value);
+ }
const colors: AnalyticsColors = {
surface: resolveColor("--color-plot-surface"),
grid: resolveColor("--color-plot-grid"),
@@ -233,9 +278,12 @@ export const mountAnalytics = (): void => {
forest: resolveColor("--color-forest"),
accent: resolveColor("--color-accent"),
mutedText: resolveColor("--color-muted"),
+ categories,
};
colorProbe.remove();
+ const scatters: AnalyticsScatter[] = [];
+
const speedCanvas = document.getElementById("analytics-chart") as HTMLCanvasElement | null;
if (speedCanvas) {
const scatter = new AnalyticsScatter(speedCanvas, data, colors, {
@@ -245,7 +293,7 @@ export const mountAnalytics = (): void => {
xUnit: "km/h",
});
window.addEventListener("resize", () => scatter.redraw());
- scatter.redraw();
+ scatters.push(scatter);
}
const durationCanvas = document.getElementById("analytics-duration-chart") as HTMLCanvasElement | null;
@@ -257,7 +305,20 @@ export const mountAnalytics = (): void => {
xUnit: "",
});
window.addEventListener("resize", () => scatter.redraw());
- scatter.redraw();
+ scatters.push(scatter);
+ }
+
+ for (const scatter of scatters) scatter.redraw();
+
+ const colorSelect = document.getElementById("analytics-color-by") as HTMLSelectElement | null;
+ if (colorSelect) {
+ const applyColorBy = (): void => {
+ const colorBy = colorSelect.value as ColorBy;
+ for (const scatter of scatters) scatter.setColorBy(colorBy);
+ renderLegend(data, colorBy, colors.categories);
+ };
+ colorSelect.addEventListener("change", applyColorBy);
+ applyColorBy();
}
};