From c8532b4750fd6b072dab25fccd0dd91fa09b4951 Mon Sep 17 00:00:00 2001 From: stweedo Date: Fri, 20 Jun 2025 12:10:14 -0500 Subject: [PATCH 1/6] recorder: Add interactive maps with clickable track points and data charts --- apps/recorder/README.md | 25 +- apps/recorder/interface.html | 927 +++++++++++++++++++++++++++-------- 2 files changed, 754 insertions(+), 198 deletions(-) diff --git a/apps/recorder/README.md b/apps/recorder/README.md index 9c283bb7e6..f24d6d5fb5 100644 --- a/apps/recorder/README.md +++ b/apps/recorder/README.md @@ -29,12 +29,29 @@ Some apps like the [Run app](https://banglejs.com/apps/?id=run) are able to auto as well. They need to define a `foobar.recorder.js` file - see the `getRecorders` function in `lib.js` for more information. -## Graphing +## Viewing and Downloading Data -You can download the information to the PC using [the App Loader](https://banglejs.com/apps/?id=recorder). Connect -to your Bangle, then in `My Apps` click the disk icon next to the `Recorder` app to download data. +You can download and visualize the information using [the App Loader](https://banglejs.com/apps/?id=recorder). Connect +to your Bangle, then in `My Apps` click the disk icon next to the `Recorder` app to access the download interface. -You can also view some information on the watch. +### Interactive Web Interface + +The download interface provides individual track visualization with: + +* **Interactive Leaflet maps** - Each GPS track gets its own map using OpenStreetMap tiles +* **Track statistics** - Distance, duration, and track points automatically calculated +* **Start/End markers** - Green circles mark track start, red circles mark end points +* **Interactive track points** - Click anywhere along the GPS track to see detailed data at that point +* **Data popups** - View available data for each point (time, heart rate, altitude, speed, steps, battery, barometer - if recorded) +* **Interactive charts** - Collapsible graphs for heart rate, battery, steps, elevation, speed, and barometer data with PNG export +* **Download options** - KML, GPX, and CSV formats for individual tracks or all at once +* **Settings** - Option to include/exclude entries without GPS coordinates +* **Unit preferences** - Choose between metric, imperial, or auto-detect based on your locale +* **Mobile responsive** - Works well on all devices + +### On-Watch Visualization + +You can also view some information on the watch: * Tap `View Tracks` * Tap on the Track number you're interested in, and you'll see a page with information about that track... diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index 63f2d0482f..5d6834c0fc 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -1,74 +1,378 @@ + + +
-
+
+ + + - + \ No newline at end of file From 2185655f7bfc9389330db0d517178116da54fb1d Mon Sep 17 00:00:00 2001 From: stweedo Date: Fri, 27 Jun 2025 04:46:21 -0500 Subject: [PATCH 2/6] recorder: prioritize barometer altitude over GPS for elevation profile --- apps/recorder/interface.html | 1811 +++++++++++++++++----------------- 1 file changed, 914 insertions(+), 897 deletions(-) diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index 5d6834c0fc..6272888382 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -1,898 +1,915 @@ - - - - - - - - -
-
- - - - - - - - - + + + + + + + + +
+
+ + + + + + + + + \ No newline at end of file From 1ed5594c0ff840b7515e76878bcf67ad687919b9 Mon Sep 17 00:00:00 2001 From: stweedo Date: Fri, 27 Jun 2025 05:16:49 -0500 Subject: [PATCH 3/6] recorder: show both barometer and GPS altitude in elevation chart --- apps/recorder/interface.html | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index 6272888382..0c5a95f835 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -201,23 +201,14 @@ scales: { y: { min: 0, max: 100, title: 'Battery %' }, y1: { position: 'right', min: 3.0, max: 4.2, title: 'Voltage (V)', grid: false } } }, steps: { filter: d => d.Steps !== undefined && d.Steps !== "", data: d => parseInt(d.Steps) || 0, label: 'Steps per Interval', color: '#36a2eb', title: 'Step Count Over Time', type: 'bar', cumulative: true }, - elevation: { - filter: d => (d['Barometer Altitude'] && d['Barometer Altitude'] !== "" && !isNaN(parseFloat(d['Barometer Altitude']))) || (d.Altitude && d.Altitude !== "" && !isNaN(parseFloat(d.Altitude))), - data: d => { - if (d['Barometer Altitude'] && d['Barometer Altitude'] !== "" && !isNaN(parseFloat(d['Barometer Altitude']))) { - return convertElevation(parseFloat(d['Barometer Altitude'])).value; - } else { - return convertElevation(parseFloat(d.Altitude)).value; - } - }, - label: () => convertElevation(1).label, - color: '#8b5cf6', + elevation: { + filter: d => (d['Barometer Altitude'] !== undefined && d['Barometer Altitude'] !== "") || (d.Altitude !== undefined && d.Altitude !== ""), title: 'Elevation Profile', - getTitle: data => { - // Check if we have any barometer altitude data - const hasBarometerAlt = data.some(d => d['Barometer Altitude'] && d['Barometer Altitude'] !== "" && !isNaN(parseFloat(d['Barometer Altitude']))); - return hasBarometerAlt ? 'Elevation Profile (Barometer)' : 'Elevation Profile (GPS)'; - } + datasets: [ + { key: 'Barometer Altitude', label: () => 'Barometer (' + convertElevation(1).unit + ')', color: '#8b5cf6', yAxis: 'y', convert: convertElevation }, + { key: 'Altitude', label: () => 'GPS (' + convertElevation(1).unit + ')', color: '#a855f7', yAxis: 'y', convert: convertElevation } + ], + scales: { y: { title: () => convertElevation(1).label } } }, speed: { filter: d => d.Latitude && d.Longitude && d.Latitude !== "" && d.Longitude !== "", calculate: true, label: () => convertSpeed(1).label, color: '#f59e0b', title: 'Speed Over Time' }, barometer: { @@ -252,7 +243,7 @@ .filter(ds => data.some(pt => pt[ds.key] !== undefined && pt[ds.key] !== "")) .map(ds => makeDataset(ds.label, data.map(pt => { const val = parseFloat(pt[ds.key]); - return val ? (ds.convert ? ds.convert(val).value : val) : null; + return val ? (ds.convert ? parseFloat(ds.convert(val).value.toFixed(1)) : val) : null; }), ds.color, { fill: false, yAxisID: ds.yAxis })); } else if (type === 'speed') { // Calculate speed from GPS coordinates @@ -292,7 +283,7 @@ interaction: { intersect: false, mode: 'point' }, plugins: { title: { display: true, text: config.getTitle ? config.getTitle(data) : config.title }, - legend: { display: datasets.length > 1 }, + legend: { display: true }, tooltip: { mode: 'nearest', intersect: false } }, scales: { @@ -520,7 +511,10 @@ const items = []; if (pointData.Time) items.push(`🕐 ${pointData.Time.toLocaleTimeString()}`); if (pointData.Heartrate) items.push(`❤️ ${pointData.Heartrate} BPM`); - if (pointData.Altitude) { + if (pointData['Barometer Altitude'] && pointData['Barometer Altitude'] !== "" && !isNaN(parseFloat(pointData['Barometer Altitude']))) { + const e = convertElevation(parseFloat(pointData['Barometer Altitude'])); + items.push(`⛰️ ${e.value.toFixed(0)} ${e.unit}`); + } else if (pointData.Altitude) { const e = convertElevation(parseFloat(pointData.Altitude)); items.push(`⛰️ ${e.value.toFixed(0)} ${e.unit}`); } From cf7abc53a90eff5451c4ee6a807c2e6574430ae1 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Fri, 27 Jun 2025 07:47:18 -0500 Subject: [PATCH 4/6] recorder: Remove confirmation before delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The confirmation wasn’t showing on mobile so there was no way to delete a track. Switched back to the original delete immediately behavior. --- apps/recorder/interface.html | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index 0c5a95f835..0830f89ae2 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -829,13 +829,11 @@

Settings

switch(task) { case "delete": - if (confirm(`Are you sure you want to delete Track ${trackid}?`)) { - Util.showModal(`Deleting ${filename}...`); - Util.eraseStorageFile(filename, () => { - Util.hideModal(); - getTrackList(); - }); - } + Util.showModal(`Deleting ${filename}...`); + Util.eraseStorageFile(filename, () => { + Util.hideModal(); + getTrackList(); + }); break; case "downloadkml": downloadTrack(filename, track => saveKML(track, `Bangle.js Track ${trackid}`)); @@ -906,4 +904,4 @@

Settings

- \ No newline at end of file + From ae661f30dec38b5fb8762a062a59b36247129d36 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:11:23 -0500 Subject: [PATCH 5/6] Update interface.html to adjust chart colors and dot size --- apps/recorder/interface.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index 0830f89ae2..3426fd389f 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -205,12 +205,12 @@ filter: d => (d['Barometer Altitude'] !== undefined && d['Barometer Altitude'] !== "") || (d.Altitude !== undefined && d.Altitude !== ""), title: 'Elevation Profile', datasets: [ - { key: 'Barometer Altitude', label: () => 'Barometer (' + convertElevation(1).unit + ')', color: '#8b5cf6', yAxis: 'y', convert: convertElevation }, - { key: 'Altitude', label: () => 'GPS (' + convertElevation(1).unit + ')', color: '#a855f7', yAxis: 'y', convert: convertElevation } + { key: 'Barometer Altitude', label: () => 'Barometer (' + convertElevation(1).unit + ')', color: '#14b8a6', yAxis: 'y', convert: convertElevation }, + { key: 'Altitude', label: () => 'GPS (' + convertElevation(1).unit + ')', color: '#9333ea', yAxis: 'y', convert: convertElevation } ], scales: { y: { title: () => convertElevation(1).label } } }, - speed: { filter: d => d.Latitude && d.Longitude && d.Latitude !== "" && d.Longitude !== "", calculate: true, label: () => convertSpeed(1).label, color: '#f59e0b', title: 'Speed Over Time' }, + speed: { filter: d => d.Latitude && d.Longitude && d.Latitude !== "" && d.Longitude !== "", calculate: true, label: () => convertSpeed(1).label, color: '#10b981', title: 'Speed Over Time' }, barometer: { filter: d => d['Barometer Temperature'] !== undefined || d['Barometer Pressure'] !== undefined, title: 'Barometer Data Over Time', @@ -290,7 +290,7 @@ x: { title: { display: true, text: 'Time' } }, y: { beginAtZero: type !== 'heartrate', title: { display: true, text: typeof config.label === 'function' ? config.label() : config.label || 'Value' } } }, - elements: { point: { radius: 2, hoverRadius: 4 } } + elements: { point: { radius: 1, hoverRadius: 3 } } } }; From 78eca5030aaf889d23208992fc84102171d1b74c Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 28 Jun 2025 11:44:13 -0500 Subject: [PATCH 6/6] Update interface.html to add confirmation before track delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of showing another popup for track delete confirmation, it now simply changes the button text to “Confirm Delete” for 3 seconds and a second tap will actually perform the delete. --- apps/recorder/interface.html | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index 3426fd389f..5b8ff0b88c 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -829,11 +829,29 @@

Settings

switch(task) { case "delete": - Util.showModal(`Deleting ${filename}...`); - Util.eraseStorageFile(filename, () => { - Util.hideModal(); - getTrackList(); - }); + if (button.dataset.confirmDelete === "true") { + // Second click - proceed with deletion + Util.showModal(`Deleting ${filename}...`); + Util.eraseStorageFile(filename, () => { + Util.hideModal(); + getTrackList(); + }); + } else { + // First click - change to confirm state + const originalText = button.textContent; + button.textContent = "Confirm Delete"; + button.classList.add("btn-error"); + button.dataset.confirmDelete = "true"; + + // Reset after 3 seconds + setTimeout(() => { + if (button.dataset.confirmDelete === "true") { + button.textContent = originalText; + button.classList.remove("btn-error"); + delete button.dataset.confirmDelete; + } + }, 3000); + } break; case "downloadkml": downloadTrack(filename, track => saveKML(track, `Bangle.js Track ${trackid}`));