Add a Distance/Time toggle to the profile chart's x-axis
CI / check (push) Successful in 42s
CI / deploy (push) Successful in 16s

TrackStats.build_profile now also computes elapsed_secs per point
(relative to the first known timestamp), alongside the existing
cumulative distance. Point x-positions stay distance-based either
way — only the shared axis strip's 5 tick labels (and the tooltip)
switch between the two, so toggling is a cheap re-render rather than
rebuilding the charts. The toggle only appears when a track actually
has elapsed-time data to switch to.
This commit is contained in:
randy committed 2026-09-07 13:54:49 +02:00
1 parent 4f7fe3c2a4
commit a0476f1204
7 files changed
+138 -14

No files matched your search

+46
View File
@@ -619,6 +619,52 @@ html, body {
bottom: 2px;
}
.profile-panel__xaxis-toggle {
display: flex;
justify-content: flex-end;
gap: 4px;
margin-bottom: 4px;
}
.profile-panel__xaxis-toggle-btn {
border: 1px solid #e1e0d9;
background: #fcfcfb;
color: #52514e;
font-size: 11px;
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
}
.profile-panel__xaxis-toggle-btn.active {
background: #0b0b0b;
border-color: #0b0b0b;
color: #fff;
}
.profile-panel__xaxis {
position: relative;
height: 14px;
margin-top: 6px;
}
.profile-panel__xaxis-tick {
position: absolute;
top: 0;
transform: translateX(-50%);
font-size: 10px;
color: #898781;
white-space: nowrap;
}
.profile-panel__xaxis-tick:first-child {
transform: translateX(0);
}
.profile-panel__xaxis-tick:last-child {
transform: translateX(-100%);
}
.profile-panel__tooltip {
position: absolute;
top: 0;
+47 -1
View File
@@ -55,9 +55,23 @@ export class TrackProfilePanel {
this._tooltip.hidden = true;
this._chartsContainer.appendChild(this._tooltip);
// Only offer the toggle when there's actually elapsed-time data to
// switch to — plenty of GPX files have no <time> at all.
this._hasTime = this._profile.some((p) => typeof p.elapsed_secs === "number");
this._xAxisMode = "distance";
if (this._hasTime) this._chartsContainer.appendChild(this._renderXAxisToggle());
this._activeSeries = SERIES.filter((s) => this._profile.some((p) => typeof p[s.key] === "number"));
this._activeSeries.forEach((series) => this._chartsContainer.appendChild(this._renderChart(series)));
// One shared strip below the whole stack, not per-chart — every chart
// above uses the same x-scale (distance-position), so a separate axis
// per chart would just repeat itself.
this._xAxisStripEl = document.createElement("div");
this._xAxisStripEl.className = "profile-panel__xaxis";
this._chartsContainer.appendChild(this._xAxisStripEl);
this._renderXAxisTicks();
this._chartsContainer.addEventListener("pointermove", this._onPointerMove);
this._chartsContainer.addEventListener("pointerleave", this._onPointerLeave);
@@ -108,6 +122,37 @@ export class TrackProfilePanel {
return wrapper;
}
_renderXAxisToggle() {
const row = document.createElement("div");
row.className = "profile-panel__xaxis-toggle";
row.innerHTML = `
<button type="button" class="profile-panel__xaxis-toggle-btn" data-mode="distance">Distance</button>
<button type="button" class="profile-panel__xaxis-toggle-btn" data-mode="time">Time</button>
`;
row.querySelectorAll("button").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.mode === this._xAxisMode);
btn.addEventListener("click", () => {
if (btn.dataset.mode === this._xAxisMode) return;
this._xAxisMode = btn.dataset.mode;
row.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b === btn));
this._renderXAxisTicks();
});
});
return row;
}
_renderXAxisTicks() {
const TICK_COUNT = 5;
this._xAxisStripEl.innerHTML = Array.from({ length: TICK_COUNT }, (_, i) => {
const fraction = i / (TICK_COUNT - 1);
const point = this._profile[nearestIndex(this._profile, fraction * this._maxDistance)];
const label = this._xAxisMode === "time" && typeof point.elapsed_secs === "number"
? formatDuration(point.elapsed_secs)
: `${(point.distance_meters / 1000).toFixed(1)} km`;
return `<span class="profile-panel__xaxis-tick" style="left:${(fraction * 100).toFixed(2)}%">${escapeHtml(label)}</span>`;
}).join("");
}
// Bound once (arrow fields, not prototype methods) so add/removeEventListener see the same reference.
_onPointerMove = (event) => {
const rect = this._chartsContainer.getBoundingClientRect();
@@ -158,7 +203,8 @@ export class TrackProfilePanel {
.filter((s) => typeof point[s.key] === "number")
.map((s) => `<div><span class="profile-panel__tooltip-label">${escapeHtml(s.label)}</span> <strong>${formatValue(point[s.key], s)}</strong></div>`)
.join("");
this._tooltip.innerHTML = `<div class="profile-panel__tooltip-distance">${(point.distance_meters / 1000).toFixed(2)} km</div>${rows}`;
const timeSuffix = typeof point.elapsed_secs === "number" ? ` · ${formatDuration(point.elapsed_secs)}` : "";
this._tooltip.innerHTML = `<div class="profile-panel__tooltip-distance">${(point.distance_meters / 1000).toFixed(2)} km${timeSuffix}</div>${rows}`;
this._tooltip.hidden = false;
const containerRect = this._chartsContainer.getBoundingClientRect();