diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-17 09:06:41 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-17 09:06:41 +0200 |
| commit | 2a20ea69a3d62c32c09e62e756a872a3042372b2 (patch) | |
| tree | e57fb5a35ba19ae252fb32f7f95e9f585d7973d7 /web/frontend | |
| parent | fcbfad33a8e8fdb671d2907b55a8074df9a558e6 (diff) | |
feat(web): refine ride profile climb labels
Diffstat (limited to 'web/frontend')
| -rw-r--r-- | web/frontend/ride-detail-canvas.ts | 19 | ||||
| -rw-r--r-- | web/frontend/ride-detail-logic.test.ts | 3 | ||||
| -rw-r--r-- | web/frontend/ride-detail-logic.ts | 5 | ||||
| -rw-r--r-- | web/frontend/ride-detail.ts | 2 | ||||
| -rw-r--r-- | web/frontend/types.d.ts | 2 |
5 files changed, 24 insertions, 7 deletions
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; |