many updates
This commit is contained in:
@@ -65,6 +65,9 @@
|
||||
<button class="btn btn-success" id="create-segment-btn" onclick="toggleSegmentMode()">
|
||||
<i class="bi bi-bezier2"></i> Create Segment
|
||||
</button>
|
||||
<a href="/discovery?activity_id={{ activity_id }}" class="btn btn-outline-info" title="Discover Segments">
|
||||
<i class="bi bi-search"></i> Discovery
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,6 +234,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Respiration -->
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 metric-card border-info">
|
||||
<div class="card-header bg-info text-white">Respiration</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between mb-2"><span>Avg:</span> <strong id="m-avg-resp">-</strong>
|
||||
br/min</div>
|
||||
<div class="d-flex justify-content-between"><span>Max:</span> <strong id="m-max-resp">-</strong> br/min
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bike Info (New) -->
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 metric-card border-light shadow-sm">
|
||||
@@ -249,11 +265,78 @@
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
Activity Streams
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="h5 mb-0">Activity Streams</span>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="form-check form-switch me-3">
|
||||
<input class="form-check-input" type="checkbox" id="smooth-toggle" onchange="toggleSmoothing()">
|
||||
<label class="form-check-label" for="smooth-toggle">Smooth Data</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="openChartModal()">
|
||||
<i class="bi bi-arrows-fullscreen"></i> Full Screen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="streams-chart" style="max-height: 400px;"></canvas>
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="card-title fw-bold mb-1">Cycling Workout Analysis</h4>
|
||||
<p class="text-muted small mb-0">Toggle metrics to customize view</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="form-check form-switch me-3">
|
||||
<input class="form-check-input" type="checkbox" id="smooth-toggle"
|
||||
onchange="toggleSmoothing()">
|
||||
<label class="form-check-label" for="smooth-toggle">Smooth</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="openChartModal()">
|
||||
<i class="bi bi-arrows-fullscreen"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metric Toggles -->
|
||||
<div class="d-flex flex-wrap gap-2 mb-4" id="chart-toggles">
|
||||
<!-- Buttons injected by JS -->
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
<div style="height: 600px; position: relative;">
|
||||
<canvas id="streams-chart"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Summary Footer -->
|
||||
<div class="row g-3 mt-4 pt-4 border-top" id="chart-footer">
|
||||
<!-- Stats injected by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Screen Chart Modal -->
|
||||
<div class="modal fade" id="chartModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-fullscreen">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Activity Streams (Full Screen)</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="form-check form-switch me-3">
|
||||
<input class="form-check-input" type="checkbox" id="modal-smooth-toggle"
|
||||
onchange="toggleModalSmoothing()">
|
||||
<label class="form-check-label" for="modal-smooth-toggle">Smooth Data</label>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body bg-light">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div style="height: 100%; width: 100%;">
|
||||
<canvas id="modal-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -309,156 +392,338 @@
|
||||
}
|
||||
|
||||
let chartInstance = null;
|
||||
let modalChartInstance = null;
|
||||
let streamData = null; // Store fetched data
|
||||
|
||||
async function loadCharts() {
|
||||
try {
|
||||
const res = await fetch(`/api/activities/${activityId}/streams`);
|
||||
if (!res.ok) return; // No streams
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.time || data.time.length === 0) return;
|
||||
streamData = data; // Cache for modal
|
||||
|
||||
const ctx = document.getElementById('streams-chart').getContext('2d');
|
||||
|
||||
// Prepare datasets
|
||||
const datasets = [];
|
||||
|
||||
if (data.heart_rate && data.heart_rate.some(x => x)) {
|
||||
datasets.push({
|
||||
label: 'Heart Rate (bpm)',
|
||||
data: data.heart_rate,
|
||||
borderColor: 'rgb(220, 53, 69)',
|
||||
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
||||
yAxisID: 'y-hr',
|
||||
tension: 0.2,
|
||||
pointRadius: 0
|
||||
});
|
||||
}
|
||||
|
||||
if (data.speed && data.speed.some(x => x)) {
|
||||
// Convert m/s to km/h
|
||||
const speedKmh = data.speed.map(s => s ? s * 3.6 : null);
|
||||
datasets.push({
|
||||
label: 'Speed (km/h)',
|
||||
data: speedKmh,
|
||||
borderColor: 'rgb(13, 110, 253)',
|
||||
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
||||
yAxisID: 'y-speed',
|
||||
tension: 0.2,
|
||||
pointRadius: 0
|
||||
});
|
||||
}
|
||||
|
||||
if (data.power && data.power.some(x => x)) {
|
||||
datasets.push({
|
||||
label: 'Power (W)',
|
||||
data: data.power,
|
||||
borderColor: 'rgb(255, 193, 7)',
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.1)',
|
||||
yAxisID: 'y-power',
|
||||
tension: 0.2,
|
||||
pointRadius: 0
|
||||
});
|
||||
}
|
||||
|
||||
if (data.altitude && data.altitude.some(x => x)) {
|
||||
datasets.push({
|
||||
label: 'Elevation (m)',
|
||||
data: data.altitude,
|
||||
borderColor: 'rgb(25, 135, 84)',
|
||||
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
||||
yAxisID: 'y-ele',
|
||||
tension: 0.2,
|
||||
pointRadius: 0,
|
||||
fill: true
|
||||
});
|
||||
}
|
||||
|
||||
if (data.cadence && data.cadence.some(x => x)) {
|
||||
datasets.push({
|
||||
label: 'Cadence (rpm)',
|
||||
data: data.cadence,
|
||||
borderColor: 'rgb(108, 117, 125)',
|
||||
backgroundColor: 'rgba(108, 117, 125, 0.1)',
|
||||
yAxisID: 'y-cad',
|
||||
tension: 0.2,
|
||||
pointRadius: 0,
|
||||
hidden: true
|
||||
});
|
||||
}
|
||||
|
||||
// Seconds to H:M:S format for labels
|
||||
const labels = data.time.map(t => {
|
||||
const h = Math.floor(t / 3600);
|
||||
const m = Math.floor((t % 3600) / 60);
|
||||
return h > 0 ? `${h}h${m}m` : `${m}m`;
|
||||
});
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { maxTicksLimit: 10 }
|
||||
},
|
||||
'y-hr': {
|
||||
type: 'linear',
|
||||
display: !!datasets.find(d => d.yAxisID === 'y-hr'),
|
||||
position: 'right',
|
||||
title: { display: true, text: 'Heart Rate' },
|
||||
grid: { drawOnChartArea: false }
|
||||
},
|
||||
'y-power': {
|
||||
type: 'linear',
|
||||
display: !!datasets.find(d => d.yAxisID === 'y-power'),
|
||||
position: 'left',
|
||||
title: { display: true, text: 'Power' }
|
||||
},
|
||||
'y-speed': {
|
||||
type: 'linear',
|
||||
display: !!datasets.find(d => d.yAxisID === 'y-speed'),
|
||||
position: 'right',
|
||||
title: { display: true, text: 'Speed' },
|
||||
grid: { drawOnChartArea: false }
|
||||
},
|
||||
'y-ele': {
|
||||
type: 'linear',
|
||||
display: !!datasets.find(d => d.yAxisID === 'y-ele'),
|
||||
position: 'right',
|
||||
title: { display: true, text: 'Elevation' },
|
||||
grid: { drawOnChartArea: false }
|
||||
},
|
||||
'y-cad': {
|
||||
type: 'linear',
|
||||
display: !!datasets.find(d => d.yAxisID === 'y-cad'),
|
||||
position: 'right',
|
||||
title: { display: true, text: 'Cadence' },
|
||||
grid: { drawOnChartArea: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
renderChart('streams-chart', data);
|
||||
renderToggles();
|
||||
renderFooter(data);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Chart load failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
let isSmoothingEnabled = false;
|
||||
|
||||
// Visibility state (managed manually to sync custom buttons with chart)
|
||||
let metricVisibility = {
|
||||
heart_rate: true,
|
||||
speed: true,
|
||||
power: true,
|
||||
altitude: false, // Match user preference
|
||||
cadence: false,
|
||||
respiration_rate: false
|
||||
};
|
||||
|
||||
const metricConfig = {
|
||||
heart_rate: { label: 'Heart Rate', icon: '❤️', color: '#ef4444' },
|
||||
speed: { label: 'Speed', icon: '🚴', color: '#3b82f6' },
|
||||
power: { label: 'Power', icon: '⚡', color: '#f97316' },
|
||||
altitude: { label: 'Elevation', icon: '⛰️', color: '#16a34a' },
|
||||
cadence: { label: 'Cadence', icon: '🔄', color: '#a855f7' },
|
||||
respiration_rate: { label: 'Resp', icon: '🫁', color: '#0dcaf0' }
|
||||
};
|
||||
|
||||
function toggleMetric(key) {
|
||||
metricVisibility[key] = !metricVisibility[key];
|
||||
renderToggles();
|
||||
if (chartInstance) updateChartVisibility(chartInstance);
|
||||
if (modalChartInstance) updateChartVisibility(modalChartInstance);
|
||||
}
|
||||
|
||||
function renderToggles() {
|
||||
const container = document.getElementById('chart-toggles');
|
||||
if (!container) return;
|
||||
|
||||
let html = '';
|
||||
Object.keys(metricConfig).forEach(key => {
|
||||
// Only show toggle if data exists
|
||||
if (streamData && streamData[key] && streamData[key].some(x => x)) {
|
||||
const cfg = metricConfig[key];
|
||||
const active = metricVisibility[key];
|
||||
const bg = active ? cfg.color : '#e5e7eb';
|
||||
const fg = active ? 'white' : '#4b5563';
|
||||
const shadow = active ? '0 4px 6px rgba(0,0,0,0.1)' : 'none';
|
||||
|
||||
html += `<button onclick="toggleMetric('${key}')" style="
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: ${bg};
|
||||
color: ${fg};
|
||||
box-shadow: ${shadow};
|
||||
transition: all 0.2s;
|
||||
">${cfg.icon} ${cfg.label}</button>`;
|
||||
}
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderFooter(data) {
|
||||
const container = document.getElementById('chart-footer');
|
||||
if (!container) return;
|
||||
|
||||
function avg(arr) {
|
||||
const valid = arr.filter(x => x !== null);
|
||||
return valid.length ? (valid.reduce((a, b) => a + b, 0) / valid.length) : 0;
|
||||
}
|
||||
|
||||
const stats = [];
|
||||
if (data.heart_rate) stats.push({ label: 'Avg HR', val: Math.round(avg(data.heart_rate)) + ' bpm', color: metricConfig.heart_rate.color });
|
||||
if (data.speed) stats.push({ label: 'Avg Speed', val: (avg(data.speed) * 3.6).toFixed(1) + ' km/h', color: metricConfig.speed.color });
|
||||
if (data.power) stats.push({ label: 'Avg Power', val: Math.round(avg(data.power)) + ' W', color: metricConfig.power.color });
|
||||
|
||||
// Elevation Gain (Approx)
|
||||
if (data.altitude) {
|
||||
let gain = 0;
|
||||
for (let i = 1; i < data.altitude.length; i++) {
|
||||
if (data.altitude[i] > data.altitude[i - 1]) gain += (data.altitude[i] - data.altitude[i - 1]);
|
||||
}
|
||||
stats.push({ label: 'Total Climb', val: Math.round(gain) + ' m', color: metricConfig.altitude.color });
|
||||
}
|
||||
|
||||
if (data.cadence) stats.push({ label: 'Avg Cadence', val: Math.round(avg(data.cadence)) + ' rpm', color: metricConfig.cadence.color });
|
||||
|
||||
container.innerHTML = stats.map(s => `
|
||||
<div class="col-md-2 text-center">
|
||||
<p style="font-size: 12px; color: #6b7280; margin-bottom: 4px;">${s.label}</p>
|
||||
<p style="font-size: 18px; font-weight: bold; color: ${s.color}; margin: 0;">${s.val}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function updateChartVisibility(chart) {
|
||||
chart.data.datasets.forEach(ds => {
|
||||
ds.hidden = !metricVisibility[ds.rawKey];
|
||||
});
|
||||
chart.update();
|
||||
}
|
||||
|
||||
function toggleSmoothing() {
|
||||
isSmoothingEnabled = document.getElementById('smooth-toggle').checked;
|
||||
const modalToggle = document.getElementById('modal-smooth-toggle');
|
||||
if (modalToggle) modalToggle.checked = isSmoothingEnabled;
|
||||
if (streamData) {
|
||||
renderChart('streams-chart', streamData);
|
||||
if (modalChartInstance) renderChart('modal-chart', streamData, true);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleModalSmoothing() {
|
||||
isSmoothingEnabled = document.getElementById('modal-smooth-toggle').checked;
|
||||
const mainToggle = document.getElementById('smooth-toggle');
|
||||
if (mainToggle) mainToggle.checked = isSmoothingEnabled;
|
||||
if (streamData) renderChart('modal-chart', streamData, true);
|
||||
}
|
||||
|
||||
function movingAverage(data, windowSize) {
|
||||
if (!data || data.length === 0) return [];
|
||||
const result = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
const offset = Math.floor(windowSize / 2);
|
||||
for (let j = i - offset; j < i - offset + windowSize; j++) {
|
||||
if (j >= 0 && j < data.length && data[j] !== null) {
|
||||
sum += data[j];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
result.push(count > 0 ? sum / count : null);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function openChartModal() {
|
||||
if (!streamData) return;
|
||||
const mt = document.getElementById('modal-smooth-toggle');
|
||||
if (mt) mt.checked = isSmoothingEnabled;
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('chartModal'));
|
||||
modal.show();
|
||||
|
||||
document.getElementById('chartModal').addEventListener('shown.bs.modal', () => {
|
||||
renderChart('modal-chart', streamData, true);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
function renderChart(canvasId, rawData, isModal = false) {
|
||||
const ctx = document.getElementById(canvasId).getContext('2d');
|
||||
|
||||
if (canvasId === 'streams-chart' && chartInstance) { chartInstance.destroy(); chartInstance = null; }
|
||||
else if (canvasId === 'modal-chart' && modalChartInstance) { modalChartInstance.destroy(); modalChartInstance = null; }
|
||||
|
||||
const data = { ...rawData };
|
||||
if (isSmoothingEnabled) {
|
||||
const window = 10;
|
||||
Object.keys(metricConfig).forEach(k => {
|
||||
if (data[k]) data[k] = movingAverage(data[k], window);
|
||||
});
|
||||
}
|
||||
|
||||
const datasets = [];
|
||||
const commonOptions = { pointRadius: 0, borderWidth: 2, tension: 0.4 };
|
||||
|
||||
if (data.heart_rate && data.heart_rate.some(x => x)) {
|
||||
datasets.push({
|
||||
...commonOptions,
|
||||
label: 'Heart Rate',
|
||||
rawKey: 'heart_rate',
|
||||
data: data.heart_rate,
|
||||
borderColor: metricConfig.heart_rate.color,
|
||||
backgroundColor: metricConfig.heart_rate.color,
|
||||
yAxisID: 'left',
|
||||
borderWidth: 2.5,
|
||||
hidden: !metricVisibility.heart_rate
|
||||
});
|
||||
}
|
||||
|
||||
if (data.speed && data.speed.some(x => x)) {
|
||||
datasets.push({
|
||||
...commonOptions,
|
||||
label: 'Speed',
|
||||
rawKey: 'speed',
|
||||
data: data.speed.map(s => s ? s * 3.6 : null),
|
||||
borderColor: metricConfig.speed.color,
|
||||
backgroundColor: metricConfig.speed.color,
|
||||
yAxisID: 'right',
|
||||
hidden: !metricVisibility.speed
|
||||
});
|
||||
}
|
||||
|
||||
if (data.power && data.power.some(x => x)) {
|
||||
datasets.push({
|
||||
...commonOptions,
|
||||
label: 'Power',
|
||||
rawKey: 'power',
|
||||
data: data.power,
|
||||
borderColor: metricConfig.power.color,
|
||||
backgroundColor: metricConfig.power.color,
|
||||
yAxisID: 'right',
|
||||
hidden: !metricVisibility.power
|
||||
});
|
||||
}
|
||||
|
||||
if (data.altitude && data.altitude.some(x => x)) {
|
||||
datasets.push({
|
||||
...commonOptions,
|
||||
label: 'Elevation',
|
||||
rawKey: 'altitude',
|
||||
data: data.altitude,
|
||||
borderColor: metricConfig.altitude.color,
|
||||
backgroundColor: metricConfig.altitude.color,
|
||||
yAxisID: 'right',
|
||||
borderWidth: 2,
|
||||
hidden: !metricVisibility.altitude
|
||||
});
|
||||
}
|
||||
|
||||
if (data.cadence && data.cadence.some(x => x)) {
|
||||
datasets.push({
|
||||
...commonOptions,
|
||||
label: 'Cadence',
|
||||
rawKey: 'cadence',
|
||||
data: data.cadence,
|
||||
borderColor: metricConfig.cadence.color,
|
||||
backgroundColor: metricConfig.cadence.color,
|
||||
yAxisID: 'left',
|
||||
borderDash: [5, 5],
|
||||
borderWidth: 1.5,
|
||||
hidden: !metricVisibility.cadence
|
||||
});
|
||||
}
|
||||
|
||||
if (data.respiration_rate && data.respiration_rate.some(x => x)) {
|
||||
datasets.push({
|
||||
...commonOptions,
|
||||
label: 'Respiration',
|
||||
rawKey: 'respiration_rate',
|
||||
data: data.respiration_rate,
|
||||
borderColor: metricConfig.respiration_rate.color,
|
||||
backgroundColor: metricConfig.respiration_rate.color,
|
||||
yAxisID: 'left',
|
||||
hidden: !metricVisibility.respiration_rate
|
||||
});
|
||||
}
|
||||
|
||||
const labels = data.time.map(t => {
|
||||
const h = Math.floor(t / 3600);
|
||||
const m = Math.floor((t % 3600) / 60);
|
||||
return h > 0 ? `${h}h${m}m` : `${m}m`;
|
||||
});
|
||||
|
||||
const newChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
plugins: {
|
||||
legend: { display: false }, // Using custom toggles
|
||||
tooltip: {
|
||||
backgroundColor: 'white',
|
||||
titleColor: '#374151',
|
||||
bodyColor: '#4b5563',
|
||||
borderColor: '#d1d5db',
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
boxPadding: 4,
|
||||
callbacks: {
|
||||
labelColor: function (context) {
|
||||
return {
|
||||
borderColor: context.dataset.borderColor,
|
||||
backgroundColor: context.dataset.borderColor
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { maxTicksLimit: isModal ? 20 : 10, maxRotation: 0 },
|
||||
grid: { display: true, drawOnChartArea: true, drawTicks: true, color: '#f3f4f6' }
|
||||
},
|
||||
left: {
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
grid: { display: true, color: '#e5e7eb', borderDash: [3, 3] },
|
||||
min: 0
|
||||
},
|
||||
right: {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
grid: { display: false },
|
||||
min: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (canvasId === 'streams-chart') chartInstance = newChart;
|
||||
else if (canvasId === 'modal-chart') modalChartInstance = newChart;
|
||||
}
|
||||
|
||||
async function loadDetails() {
|
||||
try {
|
||||
const res = await fetch(`/api/activities/${activityId}/details`);
|
||||
if (!res.ok) throw new Error("Failed to load details");
|
||||
const data = await res.json();
|
||||
window.currentDbId = data.id; // Store for segment creation
|
||||
window.currentActivityType = data.activity_type;
|
||||
|
||||
// Header
|
||||
document.getElementById('act-name').textContent = data.activity_name || 'Untitled Activity';
|
||||
@@ -494,6 +759,9 @@
|
||||
// Cadence
|
||||
document.getElementById('m-avg-cad').textContent = data.avg_cadence || '-';
|
||||
document.getElementById('m-max-cad').textContent = data.max_cadence || '-';
|
||||
// Respiration
|
||||
document.getElementById('m-avg-resp').textContent = data.avg_respiration_rate ? data.avg_respiration_rate.toFixed(1) : '-';
|
||||
document.getElementById('m-max-resp').textContent = data.max_respiration_rate ? data.max_respiration_rate.toFixed(1) : '-';
|
||||
|
||||
// Bike
|
||||
if (data.bike_setup) {
|
||||
@@ -798,6 +1066,7 @@
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
activity_id: window.currentDbId,
|
||||
activity_type: window.currentActivityType,
|
||||
start_index: startIndex,
|
||||
end_index: endIndex
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user