From 2a20ea69a3d62c32c09e62e756a872a3042372b2 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Mon, 17 Aug 2026 09:06:41 +0200 Subject: feat(web): refine ride profile climb labels --- web/frontend/ride-detail-canvas.ts | 19 ++++++++++++++----- web/frontend/ride-detail-logic.test.ts | 3 ++- web/frontend/ride-detail-logic.ts | 5 ++++- web/frontend/ride-detail.ts | 2 ++ web/frontend/types.d.ts | 2 ++ web/static/style.css | 4 +++- 6 files changed, 27 insertions(+), 8 deletions(-) (limited to 'web') diff --git a/web/frontend/ride-detail-canvas.ts b/web/frontend/ride-detail-canvas.ts index c61afee..23f9621 100644 --- a/web/frontend/ride-detail-canvas.ts +++ b/web/frontend/ride-detail-canvas.ts @@ -137,11 +137,20 @@ export class RideProfileCanvas { const endX = clamp(this.xForDistance(metrics.end.distanceKm), plot.left, plot.right); this.context.fillStyle = climbIndex === this.focusedClimbIndex ? this.colors.climbFocusFill : this.colors.accentFill; this.context.fillRect(startX, plot.top, Math.max(endX - startX, 1), plot.bottom - plot.top); - const label = formatClimbLabel(climb.name, metrics.category, metrics.cotacol); + const label = formatClimbLabel(climb.name, metrics.category, metrics.cotacol, climb.officialClimbId !== undefined); this.context.fillStyle = this.colors.climbLabel; this.context.textAlign = "center"; - this.context.textBaseline = "top"; - this.context.fillText(label, clamp((startX + endX) / 2, plot.left + 24, plot.right - 24), plot.top + 4); + this.context.textBaseline = "middle"; + const labelWidth = this.context.measureText(label).width; + const labelX = clamp((startX + endX) / 2, plot.left + 8, plot.right - 8); + const labelAngle = -Math.PI / 3; + const labelHeight = Math.abs(Math.sin(labelAngle)) * labelWidth + Math.abs(Math.cos(labelAngle)) * 14; + const labelY = clamp(plot.top + labelHeight / 2 + 4, plot.top + labelHeight / 2, plot.bottom - labelHeight / 2); + this.context.save(); + this.context.translate(labelX, labelY); + this.context.rotate(labelAngle); + this.context.fillText(label, 0, 0); + this.context.restore(); } for (const crossing of this.profile.crossings || []) { @@ -161,7 +170,7 @@ export class RideProfileCanvas { for (const point of this.points) this.context.lineTo(this.xForDistance(point.distanceKm), this.yForElevation(point.elevationM)); this.context.lineTo(this.xForDistance(this.points[this.points.length - 1].distanceKm), plot.bottom); this.context.closePath(); - this.context.fillStyle = this.colors.accentFill; + this.context.fillStyle = this.colors.profileFill; this.context.fill(); this.context.beginPath(); for (let index = 0; index < this.points.length; index++) { @@ -169,7 +178,7 @@ export class RideProfileCanvas { if (index === 0) this.context.moveTo(this.xForDistance(point.distanceKm), this.yForElevation(point.elevationM)); else this.context.lineTo(this.xForDistance(point.distanceKm), this.yForElevation(point.elevationM)); } - this.context.strokeStyle = this.colors.accent; + this.context.strokeStyle = this.colors.profileLine; this.context.lineWidth = 2.5; this.context.stroke(); for (const crossing of this.profile.crossings || []) { diff --git a/web/frontend/ride-detail-logic.test.ts b/web/frontend/ride-detail-logic.test.ts index b848f7e..6f27191 100644 --- a/web/frontend/ride-detail-logic.test.ts +++ b/web/frontend/ride-detail-logic.test.ts @@ -40,9 +40,10 @@ test("calculates climb metrics with Cotacol", () => { assert.equal(metrics.category, categoryForCotacol(metrics.cotacol)); }); -test("formats profile climb labels with Cotacol", () => { +test("formats profile climb labels with category", () => { assert.equal(formatClimbLabel("", "Cat 4", 45.9), "Cat 4 45.9"); assert.equal(formatClimbLabel("Col de Test", "Cat 4", 45.9), "Col de Test"); + assert.equal(formatClimbLabel("Ventoux", "HC", 600, true), "Ventoux (HC)"); }); test("formats profile labels consistently", () => { diff --git a/web/frontend/ride-detail-logic.ts b/web/frontend/ride-detail-logic.ts index a2ba80a..f8eb040 100644 --- a/web/frontend/ride-detail-logic.ts +++ b/web/frontend/ride-detail-logic.ts @@ -28,7 +28,10 @@ export const categoryForCotacol = (cotacol: number): string => { return "HC"; }; -export const formatClimbLabel = (name: string, category: string, cotacol: number): string => name || `${category} ${cotacol.toFixed(1)}`; +export const formatClimbLabel = (name: string, category: string, cotacol: number, officialClimb = false): string => { + if (name) return officialClimb ? `${name} (${category})` : name; + return `${category} ${cotacol.toFixed(1)}`; +}; export const elevationAtDistance = (points: ProfilePoint[], index: number, distanceM: number): number => { if (index + 1 >= points.length) return points[index].elevationM; diff --git a/web/frontend/ride-detail.ts b/web/frontend/ride-detail.ts index fb75c19..d59cbe9 100644 --- a/web/frontend/ride-detail.ts +++ b/web/frontend/ride-detail.ts @@ -33,6 +33,8 @@ export const mountRideDetail = (): void => { plotSurfaceOverlay: resolveColor("--color-plot-surface-overlay"), grid: resolveColor("--color-plot-grid"), accentFill: resolveColor("--color-accent-fill"), + profileFill: resolveColor("--color-profile-fill"), + profileLine: resolveColor("--color-profile-line"), climbLabel: resolveColor("--color-climb-label"), crossing: resolveColor("--color-crossing"), crossingLabel: resolveColor("--color-crossing-label"), diff --git a/web/frontend/types.d.ts b/web/frontend/types.d.ts index 6fa9976..a5e83b8 100644 --- a/web/frontend/types.d.ts +++ b/web/frontend/types.d.ts @@ -92,6 +92,8 @@ export interface RideDetailColors { forest: string; plotSurfaceOverlay: string; accentFill: string; + profileFill: string; + profileLine: string; climbLabel: string; crossing: string; crossingLabel: string; diff --git a/web/static/style.css b/web/static/style.css index ab6c1ed..e32525d 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -32,7 +32,9 @@ --color-plot-surface-overlay: oklch(from var(--color-plot-surface) l c h / 90%); --color-plot-grid: var(--color-border-subtle); --color-accent-fill: oklch(from var(--palette-coral) l c h / 12%); - --color-climb-label: oklch(from var(--palette-coral) calc(l - 0.21) calc(c * 0.72) h); + --color-profile-fill: oklch(from var(--palette-leaf) l c h / 16%); + --color-profile-line: var(--color-forest); + --color-climb-label: oklch(40% 0.12 35deg); --color-crossing: oklch(from var(--palette-leaf) l c h / 60%); --color-crossing-label: var(--color-notice-text); --color-hover-line: oklch(from var(--palette-forest) l c h / 60%); -- cgit v1.2.3