
Cycle Analyser
Prompt
Create a cyberpunk kawaii cycle ride analyser web app. The analyser gives a file upload button which expects a CSV file with the following format: date,timestamp,ISO8601,hr,latitude,longitude,elevation,distance,lap,since_start 08:20:12.633,1742890812.633,2025-03-25T08:20:12.633Z,,,,,,, 08:20:14.633,1742890814.633,2025-03-25T08:20:14.633Z,,51.475065,0.017324,43.037,0.000,1,2 08:20:15.633,1742890815.633,2025-03-25T08:20:15.633Z,,51.475065,0.017324,43.065,0.074,1,3 08:20:16.633,1742890816.633,2025-03-25T08:20:16.633Z,,51.475071,0.017326,43.087,0.000,1,4 08:20:17.633,1742890817.633,2025-03-25T08:20:17.633Z,,51.475082,0.017328,43.109,0.502,1,5 During ingestion of the data it will discard sections where no movement took place. After analysing the CSV, the app will show overview statistics such as highest and average speed, highest and average heart rate, METS etc. It will show a map overview of the ride, with the path of the ride shown in segments of similar heart rate and coloured based on intensity. On overlay show heart rate, speed, METS etc. Surprise me with 2 interesting additional features.
A system prompt was added to support web rendering
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Neon Kawaii Cycle-Ride Analyser ⚡🚴♀️</title> <!-- Leaflet --> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-o9N1jRVv6agGLmq0u21f232XvJXWZJ7gZJN3y7rSChU=" crossorigin=""/> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-oQW2GqOgEgwG0OskekuuLaIJ3Z9z5A8MwY+2f4y/iJE=" crossorigin=""></script> <!-- Papa Parse --> <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script> <style> @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Poppins:wght@300;600&display=swap'); :root{ --neon1:#00e5ff; --neon2:#c800ff; --neon3:#ff00b7; --bg:#030920; --panel:#0d142e; } *{ box-sizing:border-box; } html,body{ margin:0; height:100%; font-family:'Poppins',sans-serif; color:#f5f5f5; background: radial-gradient(circle at 20% 20%,rgba(0,229,255,.15),transparent 60%), radial-gradient(circle at 80% 80%,rgba(255,0,183,.15),transparent 60%), var(--bg); overflow:hidden; } h1{ font-family:'Press Start 2P',cursive;font-size:1.5rem;text-align:center;margin:1rem 0; background:linear-gradient(90deg,var(--neon1),var(--neon3)); -webkit-background-clip:text;color:transparent;} #app{ display:flex; flex-direction:column; height:100%; } #controls{ padding:1rem; background:var(--panel); display:flex; flex-wrap:wrap; align-items:center; gap:.8rem; } input[type=file]{ color:#fff; } button{ background:linear-gradient(90deg,var(--neon1),var(--neon3)); border:none;padding:.6rem 1rem; color:#000;font-weight:600; cursor:pointer; border-radius:4px; transition:transform .2s; } button:hover{ transform:scale(1.05); } #stats{ padding:1rem; background:rgba(0,0,0,.25); display:flex; flex-wrap:wrap; gap:1rem; } .stat{ background:var(--panel); padding:1rem; border-radius:6px; min-width:120px; text-align:center; font-weight:600; box-shadow:0 0 8px var(--neon1); } #map{ flex:1; } /* kawaii sparkline */ #speedChart{ width:100%; height:80px; } </style> </head> <body> <h1>⚡ Cyberpunk Kawaii Ride Analyser ⚡</h1> <div id="app"> <div id="controls"> <input type="file" id="fileInput" accept=".csv"> <button id="ttsBtn" disabled>🔊 Play Ride Summary</button> <button id="gpxBtn" disabled>⬇️ Download GPX</button> </div> <canvas id="speedChart"></canvas> <div id="stats"></div> <div id="map"></div> </div> <script> // ================== GEODESY HELPERS ==================== const R = 6371; // km function toRad(d){ return d*Math.PI/180; } function haversine(lat1,lon1,lat2,lon2){ const dLat=toRad(lat2-lat1), dLon=toRad(lon2-lon1); const a=Math.sin(dLat/2)**2+ Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLon/2)**2; return 2*R*Math.asin(Math.sqrt(a)); // km } // ================== UI ELEMENTS ==================== const fileInput=document.getElementById('fileInput'); const statsDiv=document.getElementById('stats'); const ttsBtn=document.getElementById('ttsBtn'); const gpxBtn=document.getElementById('gpxBtn'); const speedCanvas=document.getElementById('speedChart'); let ctx=speedCanvas.getContext('2d'); // Leaflet map const map=L.map('map',{zoomControl:false}); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{ attribution:'© OSM contributors' }).addTo(map); // storage let summaryText=""; let gpxStr=""; fileInput.addEventListener('change',e=>{ const file=e.target.files[0]; if(!file)return; Papa.parse(file,{ header:true, dynamicTyping:true, skipEmptyLines:true, complete:res=>processData(res.data) }); }); // ================== MAIN ANALYSIS ==================== function processData(rows){ const segments=[]; let last=null,totalTime=0,totalDist=0; let speedArr=[], hrArr=[], metArr=[]; for(const r of rows){ const lat=r.latitude, lon=r.longitude; if(lat==null || lon==null) continue; const ts=r.timestamp || (new Date(r.ISO8601).getTime()/1000); if(last){ // compare const dt=ts-last.ts; let d=r.distance!=null && r.distance!==""? parseFloat(r.distance): haversine(lat,lon,last.lat,last.lon); if(d<=0) { last={lat,lon,ts,hr:r.hr}; continue; } // stationary -> discard const speedKmh=d/dt*3600; // km/h const hrNow=r.hr || last.hr || null; const MET=1+speedKmh/5; // cheeky approximation segments.push({lat1:last.lat,lon1:last.lon,lat2:lat,lon2:lon,hr:hrNow,speed:speedKmh,met:MET}); // accumulators totalTime+=dt; totalDist+=d; speedArr.push(speedKmh); metArr.push(MET); if(hrNow!=null){ hrArr.push(hrNow);} } last={lat,lon,ts,hr:r.hr}; } if(segments.length===0){ alert("No ride data found!"); return; } // ======= stats const avgSpeed = speedArr.reduce((a,b)=>a+b,0)/speedArr.length; const maxSpeed = Math.max(...speedArr); const avgHR = hrArr.length? hrArr.reduce((a,b)=>a+b,0)/hrArr.length : "–"; const maxHR = hrArr.length? Math.max(...hrArr):"–"; const avgMET = metArr.reduce((a,b)=>a+b,0)/metArr.length; renderStats({ distance: totalDist, duration: totalTime, avgSpeed, maxSpeed, avgHR, maxHR, avgMET }); drawMap(segments); drawSpeedSpark(speedArr); summaryText = `Konnichiwa, road warrior! You cycled ${totalDist.toFixed(2)} kilometres in ${(totalTime/60).toFixed(1)} minutes. `+ `Average speed ${avgSpeed.toFixed(1)} km/h (peaking at ${maxSpeed.toFixed(1)}!). `+ (hrArr.length?`Your heart hit ${maxHR} bpm, averaging ${avgHR.toFixed(0)}.`:"")+" Sugoi!`; gpxStr=buildGPX(segments); ttsBtn.disabled=false; gpxBtn.disabled=false; } // ================== RENDERERS ==================== function renderStats(o){ statsDiv.innerHTML=''; const make=(label,val,unit='')=>{ const d=document.createElement('div'); d.className='stat'; d.innerHTML=`${label}<br><span style="font-size:1.4rem;color:var(--neon1)">${val}</span> ${unit}`; statsDiv.appendChild(d); }; make("Distance",o.distance.toFixed(2),"km"); make("Duration",(o.duration/60).toFixed(1),"min"); make("Avg Speed",o.avgSpeed.toFixed(1),"km/h"); make("Top Speed",o.maxSpeed.toFixed(1),"km/h"); make("Avg HR",o.avgHR==="–"?"–":o.avgHR.toFixed(0),"bpm"); make("Max HR",o.maxHR,"bpm"); make("Avg MET",o.avgMET.toFixed(1),""); } function hrColor(hr){ if(hr==null) return "#888"; if(hr<100) return "#00e5ff"; if(hr<140) return "#00ff6f"; if(hr<160) return "#ffea00"; return "#ff004d"; } function drawMap(segs){ map.eachLayer(l=>{ if(l instanceof L.Polyline) map.removeLayer(l); }); const bounds=[]; segs.forEach(s=>{ const line=[[s.lat1,s.lon1],[s.lat2,s.lon2]]; L.polyline(line,{color:hrColor(s.hr),weight:5,opacity:0.8}) .bindPopup(`Speed ${s.speed.toFixed(1)} km/h<br>HR ${s.hr||'–'}<br>MET ${s.met.toFixed(1)}`) .addTo(map); bounds.push([s.lat1,s.lon1],[s.lat2,s.lon2]); }); map.fitBounds(bounds,{padding:[30,30]}); } function drawSpeedSpark(arr){ speedCanvas.width=speedCanvas.clientWidth; speedCanvas.height=speedCanvas.clientHeight; ctx.clearRect(0,0,speedCanvas.width,speedCanvas.height); const max=Math.max(...arr); ctx.strokeStyle='url(#grad)'; // gradient const grad=ctx.createLinearGradient(0,0,speedCanvas.width,0); grad.addColorStop(0,'var(--neon1)'); grad.addColorStop(1,'var(--neon3)'); ctx.strokeStyle=grad; ctx.lineWidth=2; ctx.beginPath(); arr.forEach((v,i)=>{ const x=i/(arr.length-1)*speedCanvas.width; const y=speedCanvas.height-(v/max*speedCanvas.height); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }); ctx.stroke(); } // ================== GPX EXPORT ==================== function buildGPX(segs){ const head=`<?xml version="1.0" encoding="UTF-8"?> <gpx version="1.1" creator="KawaiiRideAnalyser" xmlns="http://www.topografix.com/GPX/1/1"> <trk><name>Kawaii Ride</name><trkseg>`; const tail=`</trkseg></trk></gpx>`; const pts=segs.map(s=>`<trkpt lat="${s.lat1}" lon="${s.lon1}"></trkpt>`).join('\n'); return head+pts+tail; } gpxBtn.addEventListener('click',()=>{ const blob=new Blob([gpxStr],{type:'application/gpx+xml'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='ride.gpx'; a.click(); URL.revokeObjectURL(url); }); // ================== TTS COACH ==================== ttsBtn.addEventListener('click',()=>{ const utter=new SpeechSynthesisUtterance(summaryText); utter.lang='en-US'; utter.rate=1.1; utter.pitch=1.4; speechSynthesis.speak(utter); }); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cyberpunk Kawaii Cycle Ride Analyser</title> <!-- Google Fonts for Cyberpunk & Kawaii vibes --> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&family=Noto+Sans:wght@900&display=swap" rel="stylesheet"> <!-- Leaflet CSS for maps --> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <!-- SimpleKawaii icons for extra cuteness --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fontisto@v3.0.4/css/fontisto/fontisto.min.css"> <style> body { margin: 0; padding: 0; background: linear-gradient(135deg, #2dd4ff 20%, #a100ff 100%); color: #f9eaff; font-family: 'Noto Sans', sans-serif; min-height: 100vh; overflow-x: hidden; } header { display: flex; align-items: center; justify-content: center; gap: 15px; padding: 35px 0 25px 0; letter-spacing: 3px; } header h1 { font-family: 'Orbitron', 'Noto Sans', monospace; font-size: 2.5em; color: #fff0f0; text-shadow: 0 0 20px #ff64ef, 0 0 40px #00fff1; } .icon-cute { font-size: 2.6em; color: #ffb5fa; filter: drop-shadow(0 0 5px #fd0fff88); margin-bottom: -10px; } main { max-width: 1040px; margin: 0 auto; background: rgba(16, 12, 38, 0.83); border-radius: 18px; box-shadow: 0 0 40px #001fee44; padding: 32px; margin-bottom: 40px; } .file-upload { background: rgba(255,255,255,0.1); padding: 14px 18px; border-radius: 11px; box-shadow: 0 0 10px #00ddff44; margin-bottom: 18px; display: inline-block; font-size: 1.21em; color: #ffd9fa; cursor: pointer; font-weight: bold; transition: background .2s; } .file-upload:hover { background: rgba(255,255,255,0.18); box-shadow: 0 0 20px #ffd6fa88; } .stats-overview { display: flex; flex-wrap: wrap; gap: 32px 44px; margin: 32px 0 22px 0; font-size: 1.1em; } .stat-block { background: rgba(34,4,58,0.75); border: 1.5px solid #7e52ff7a; border-radius: 14px; padding: 18px 24px; min-width: 160px; text-align: center; box-shadow: 0 0 10px #e98bfe3c; display: flex; flex-direction: column; align-items: center; gap: 5px; } .stat-label { font-size: .93em; color: #ffb8fa; margin-bottom: 3px; } .stat-value { font-size: 1.38em; color: #e7faff; font-family: 'Orbitron', monospace; font-weight: 700; text-shadow: 0 0 5px #2affbe99; } .stat-unit { font-size: .87em; color: #fff7e2cc; margin-left: 4px; } #map { width: 100%; height: 440px; border-radius: 15px; box-shadow: 0 0 25px #9220ff66; margin-bottom: 30px; background: #151212; } .legend { margin: 12px 0 35px 0; display: inline-block; background: rgba(41,10,70,0.85); padding: 8px 23px 8px 12px; border-radius: 10px; font-size: 1em; } .legend-gradient { width: 130px; height: 15px; border-radius: 4px; background: linear-gradient(90deg, #44f9e0 0%, #a6e85c 30%, #ffe358 60%, #ff6c8e 90%); margin: 0 12px 0 6px; display: inline-block; vertical-align: middle; box-shadow: 0 0 6px #300f3c; } .map-tooltip { font-family: 'Noto Sans', sans-serif; font-size: 0.97em; font-weight: bold; color: #f4f2fa; background: rgba(32,11,35,0.86); padding: 7px 14px; border: 1px solid #a07be6; border-radius: 7px; box-shadow: 0 0 10px #25ffe966; } .section-title { font-family: 'Noto Sans', sans-serif; font-size: 1.2em; color: #ffe0fd; letter-spacing: 1.5px; margin-bottom: 7px; margin-top: 27px; text-shadow: 0 0 8px #23f3e899; } .feature { margin-top: 24px; background: rgba(255,185,254,0.1); border-left: 4px solid #ff83d2cc; padding: 21px 17px 13px 27px; border-radius: 10px; box-shadow: 0 0 12px #a3ebff3a; } .feature-title { font-family: 'Orbitron', monospace; color: #ff6cab; font-size: 1.1em; margin-bottom: 7px; letter-spacing: 1.2px; } @media (max-width: 780px) { main { padding: 10px; } .stats-overview { flex-direction: column; } } </style> <!-- PapaParse for CSV parsing --> <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script> <!-- Leaflet.js for mapping --> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> </head> <body> <header> <span class="icon-cute fi fi-cycling"></span> <h1>C Y B E R P U N K <span style="color:#fff881;">KAWAII</span> Cycle Ride Analyser</h1> </header> <main> <label class="file-upload"> <input type="file" id="csvInput" accept=".csv" style="display:none"> <span class="fi fi-upload"></span> Upload your ride .csv! </label> <div id="overview" style="display:none;"> <div class="section-title">Ride Overview Stats</div> <div class="stats-overview" id="stats"></div> <div class="legend" id="legend"> <span> <b>Intensity legend:</b> <span class="legend-gradient"></span> <span style="font-size:.95em;">low<span style="margin:0 15px"></span>high</span> </span> </div> <div id="map"></div> <div class="section-title">Features</div> <div class="feature" id="feature-kawaii"> <div class="feature-title"><span class="fi fi-smiling"></span> Motivational Kawaii Stickers Mode</div> <div id="kawaii-rating"></div> <ul id="kawaii-stickers" style="display:flex;gap:12px;"></ul> </div> <div class="feature" id="feature-tops"> <div class="feature-title"><span class="fi fi-signal"></span> Top Intensity Moments</div> <ul id="top-intensities"></ul> </div> </div> </main> <script> // --- Helper: Color interpolation for heart rate intensity --- function getIntensityColor(percent) { // 0 = low (aqua), 0.3 = light-green, 0.7 = yellow, 1 = pinkish const stops = [ { pct: 0.00, color:[68,249,224]}, // aqua { pct: 0.30, color:[166,232,92]}, // greeny { pct: 0.70, color:[255,227,88]}, // yellow { pct: 1.00, color:[255,108,142]} // pink ]; let i=1; for (;i<stops.length-1;i++) if (percent<stops[i].pct) break; const prev = stops[i-1]; const next = stops[i]; const range = next.pct-prev.pct; const frac = (percent-prev.pct)/range; const color = prev.color.map((v,j)=>Math.round(v + frac*(next.color[j]-v))); return `rgb(${color.join(',')})`; } // --- Helper: Simple moving average smoothing --- function movingAvg(arr, len) { let out = []; for(let i=0;i<arr.length;i++) { let s = 0, c = 0; for(let j=i-len+1;j<=i;j++) if(j>=0 && arr[j]!=null){s+=arr[j];c++;} out.push(c?s/c:null); } return out; } // --- METs for cycling, very approximate: https://golf.procon.org/met-values-for-800-activities/ --- function estimateMETs(speedKmh) { if (speedKmh < 16) return 6.8; if (speedKmh < 19) return 8.0; if (speedKmh < 22.5) return 10.0; if (speedKmh < 25.5) return 12.0; if (speedKmh < 30) return 14.0; if (speedKmh < 35) return 16.0; return 18.0; } // Kawaii stickers & feedback const kawaiiStickers = [ {min:0, max:6, txt:"Just warming up! 🚲✨", emoji:"🦄", sticker:"🌸"}, {min:6,max:8,txt:"Getting there, kawaii effort! 🧁",emoji:"🐰",sticker:"🍭"}, {min:8,max:10,txt:"Bunny Mode! Smooth ride! 🐇",emoji:"🥕",sticker:"🍓"}, {min:10,max:12,txt:"Sugoi! So much energy! 🌟",emoji:"🐱",sticker:"🍬"}, {min:12,max:15,txt:"Cyber Turbo! Bolt ahead!",emoji:"🐼",sticker:"⚡"}, {min:15,max:99,txt:"GIGA Intensity!! Superstar!",emoji:"🦊",sticker:"🚀"} ]; // -- App Logic -- let map, pathLayer, markerLayer; document.getElementById('csvInput').addEventListener('change', function(e) { if (!e.target.files.length) return; Papa.parse(e.target.files[0], { header: true, skipEmptyLines: true, complete: function(results) { const data = results.data.map(d => ({ time: d['ISO8601'] || "", lat: d['latitude'] ? parseFloat(d['latitude']) : null, lng: d['longitude'] ? parseFloat(d['longitude']) : null, hr: d['hr'] ? parseFloat(d['hr']) : null, elev: d['elevation'] ? parseFloat(d['elevation']) : null, dist: d['distance'] ? parseFloat(d['distance']) : null, lap: d['lap'] ? parseInt(d['lap']) : null, since: d['since_start'] ? parseInt(d['since_start']) : null })); // 1. Discard points where no movement occurred (distance=0 in consecutive rows) let filtered = []; for(let i=1;i<data.length;i++){ if(data[i].dist != null && data[i-1].dist != null && data[i].dist != data[i-1].dist){ filtered.push({...data[i], idx: i}); } } if(filtered.length < 2){ alert('No moving segments found in file!'); return; } // 2. Calculate per-point speed (m/s) using distance + timestamp for(let i=1;i<filtered.length;i++) { const distDelta = filtered[i].dist - filtered[i-1].dist; let t1 = Date.parse(filtered[i-1].time)/1000, t2 = Date.parse(filtered[i].time)/1000; const timeDelta = t2-t1; filtered[i].speed = (distDelta > 0 && timeDelta>0) ? (distDelta / timeDelta) : 0; } filtered[0].speed = filtered[1].speed; // Copy first valid // 3. Smooth speed (optional), not heartRate const speeds = filtered.map(x=>x.speed || null); const smoothSpeeds = movingAvg(speeds, 3); for(let i=0;i<filtered.length;i++) filtered[i].speed = smoothSpeeds[i]; // 4. Compute approximate METs for(let i=0;i<filtered.length;i++){ const kmh = filtered[i].speed*3.6; filtered[i].mets = estimateMETs(kmh); } // 5. Stat calculations const maxSpeed = Math.max(...filtered.map(x=>x.speed*3.6)); const avgSpeed = filtered.reduce((s,x)=>s+(x.speed*3.6),0)/filtered.length; const movingHRs = filtered.filter(x=>x.hr); let maxHR = movingHRs.length > 0 ? Math.max(...movingHRs.map(x=>x.hr)) : null; let avgHR = movingHRs.length > 0 ? movingHRs.reduce((s,x)=>s+x.hr,0)/movingHRs.length : null; maxHR = maxHR?Math.round(maxHR):'N/A'; avgHR = avgHR?Math.round(avgHR):'N/A'; const maxMETS = Math.max(...filtered.map(x=>x.mets)); const avgMETS = filtered.reduce((s,x)=>s+x.mets,0)/filtered.length; // 5. Ride summary time, distance, elevation gain const totDist = filtered[filtered.length-1].dist; // Assume start at 0 const rideTimeSec = Math.max(1, (Date.parse(filtered[filtered.length-1].time)-Date.parse(filtered[0].time))/1000); // elevation gain is only uphill portions let elevGain = 0; for(let i=1;i<filtered.length;i++) if(filtered[i].elev && filtered[i-1].elev && filtered[i].elev>filtered[i-1].elev) elevGain+=(filtered[i].elev-filtered[i-1].elev); // 6. Generate or update stats block document.getElementById('overview').style.display = ''; document.getElementById('stats').innerHTML = ` <div class="stat-block"> <div class="stat-label">Total Distance</div> <div class="stat-value">${totDist.toFixed(2)} <span class="stat-unit">km</span></div> </div> <div class="stat-block"> <div class="stat-label">Ride Time</div> <div class="stat-value">${Math.floor(rideTimeSec/60)}<span class="stat-unit">min</span> ${Math.round(rideTimeSec%60)}<span class="stat-unit">s</span></div> </div> <div class="stat-block"> <div class="stat-label">Elevation Gain</div> <div class="stat-value">${elevGain.toFixed(1)} <span class="stat-unit">m ↑</span></div> </div> <div class="stat-block"> <div class="stat-label">Max Speed</div> <div class="stat-value">${maxSpeed.toFixed(1)} <span class="stat-unit">km/h</span></div> </div> <div class="stat-block"> <div class="stat-label">Avg Speed</div> <div class="stat-value">${avgSpeed.toFixed(1)} <span class="stat-unit">km/h</span></div> </div> <div class="stat-block"> <div class="stat-label">Max Heart Rate</div> <div class="stat-value">${maxHR} <span class="stat-unit">bpm</span></div> </div> <div class="stat-block"> <div class="stat-label">Avg Heart Rate</div> <div class="stat-value">${avgHR} <span class="stat-unit">bpm</span></div> </div> <div class="stat-block"> <div class="stat-label">Max METs</div> <div class="stat-value">${maxMETS.toFixed(1)}</div> </div> <div class="stat-block"> <div class="stat-label">Avg METs</div> <div class="stat-value">${avgMETS.toFixed(1)}</div> </div> `; // --- MAP VISUALISATION --- // // Leaflet init if (!map){ map = L.map('map').setView([filtered[0].lat,filtered[0].lng], 15); L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap <b>|</b> Cyberpunk 🦄' }).addTo(map); } if(pathLayer){map.removeLayer(pathLayer);} if(markerLayer){map.removeLayer(markerLayer);} markerLayer = L.layerGroup().addTo(map); // Segment path into zones of similar HR (or by METs if hr not present) let segments = []; let curr = []; const baseBy = movingHRs.length>2 ? 'hr' : 'mets'; // fallback let lastVal = filtered[0][baseBy]; let zoneWidth = baseBy==='hr' ? 10 : 2.5; for(let i=0;i<filtered.length;i++){ let p = filtered[i]; if(curr.length && Math.abs((p[baseBy]||0)-(lastVal||0))>zoneWidth) { segments.push(curr); curr = []; } curr.push(p); lastVal = p[baseBy]; } if(curr.length) segments.push(curr); // Collect all values for normalization let allVals = filtered.map(x=>(x[baseBy]||0)); let minV = Math.min(...allVals), maxV = Math.max(...allVals); // Draw each segment as colored polyline segments.forEach(seg=>{ const intensity = seg[0][baseBy]!=null ? (seg[0][baseBy]-minV)/(maxV-minV+0.01) : 0; let latlngs = seg.map(p=>[p.lat,p.lng]); let col = getIntensityColor(intensity); let lyr = L.polyline(latlngs, { color: col, weight: 7, opacity: 0.87, lineCap:'round' }).addTo(map); // Overlay: add tooltips for mid-point const midIdx = Math.floor(seg.length/2); const m = seg[midIdx]; let info = ` <div class="map-tooltip"> <div><b>Time:</b> ${m.time.slice(11,19)}</div> <div><b>Speed:</b> ${(m.speed*3.6).toFixed(1)} km/h</div> <div><b>HR:</b> ${m.hr?m.hr+" bpm":"N/A"}</div> <div><b>METs:</b> ${m.mets.toFixed(1)}</div> </div> `; lyr.bindTooltip(info, {sticky: true}); }); // Show start/end markers let startM = L.marker([filtered[0].lat,filtered[0].lng],{ icon:L.divIcon({className:'',html:`<span style="font-size:2.0em; filter:drop-shadow(0 0 7px #00ffd8);">🍡</span>`,iconSize:[28,38]}) }).bindTooltip('<b>Start!</b>').addTo(markerLayer); let endM = L.marker([filtered[filtered.length-1].lat,filtered[filtered.length-1].lng],{ icon:L.divIcon({className:'',html:`<span style="font-size:2em; filter:drop-shadow(0 0 7px #ff54be);">🎀</span>`,iconSize:[28,38]}) }).bindTooltip('<b>Finish!</b>').addTo(markerLayer); // Center & fit map let bounds = L.latLngBounds(filtered.map(p=>[p.lat,p.lng])); map.fitBounds(bounds.pad(0.11)); // --- Feature 1: Kawaii "Motivational Stickers" --- // Based on mean METS: Assign rating + display animated stickers let kawaiiTier = kawaiiStickers.find(k=>avgMETS>=k.min&&avgMETS<k.max)||kawaiiStickers.at(-1); let kawaiiRating = `${kawaiiTier.emoji} <b>${kawaiiTier.txt}</b> METS: <span style="color:#fff4b2">${avgMETS.toFixed(1)}</span>`; document.getElementById('kawaii-rating').innerHTML = kawaiiRating; // Sprinkle kawaii stickers for progress bar! let stickersUL = document.getElementById('kawaii-stickers'); stickersUL.innerHTML = ''; let n = Math.min(7,Math.ceil(totDist/1) + Math.floor(avgMETS/2)); for(let i=0;i<n;i++){ let s = document.createElement('li'); s.style.fontSize = "2.1em"; s.style.transition = 'transform 0.45s'; s.innerText = kawaiiTier.sticker; if(Math.random()>0.7){ s.style.transform = `rotate(${Math.floor(-15+Math.random()*30)}deg) scale(${1+Math.random()*0.18})`; } stickersUL.appendChild(s); } // --- Feature 2: Top intensity moments --- // Slice window & show top speed/METS/HR moments let topIntensityUL = document.getElementById('top-intensities'); let maxIntensIdx = 0, maxIntensVal = -1; let intensityPer = filtered.map(x=>(x.mets/18)+(x.speed*3.6)/40+(x.hr?x.hr/200:0)); for(let i=0;i<filtered.length;i++) if(intensityPer[i]>maxIntensVal){maxIntensVal=intensityPer[i];maxIntensIdx=i;} // Populate top 3 moments let intensityMoments = filtered.slice().map((x,i)=>({ idx:i,val:intensityPer[i], info:`at ${x.time.slice(11,19)} ~ ${(x.speed*3.6).toFixed(1)}km/h, METs ${x.mets.toFixed(1)}, HR ${x.hr||'N/A'}` })); intensityMoments.sort((a,b)=>b.val-a.val); let top3 = intensityMoments.slice(0,3); topIntensityUL.innerHTML = ''; top3.forEach((e,ri)=>{ let li = document.createElement('li'); li.style.fontSize = '1.04em'; let emoji = ['🦾','✨','🚨'][ri] || '🌟'; li.innerHTML = `${emoji}<b> Moment #${ri+1}</b>: ${e.info}`; topIntensityUL.appendChild(li); // For each, show marker let mpt = filtered[e.idx]; L.marker([mpt.lat,mpt.lng],{ icon:L.divIcon({className:'',html:`<span style="font-size:1.5em; filter:drop-shadow(0 0 6px #fff553);">${emoji}</span>`,iconSize:[20,38]}) }).bindTooltip(`<b>INTENSITY #${ri+1}</b><br>${e.info}`,{permanent:false}).addTo(markerLayer); }); } }); }); // Click fake browse when uploading document.querySelector('.file-upload input').addEventListener('click', function(e) { e.target.value = ""; // For re-uploading same file }) document.querySelector('.file-upload').addEventListener('click', function(e){ document.getElementById('csvInput').click(); }) </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <title>Cyberpunk Kawaii Cycle Analyser</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://cdnjs.cloudflare.com/ajax/libs/Papa/5.4.1/papaparse.min.js"></script> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <style> body { background: #0e001f; color: #00ffcc; font-family: 'Comic Sans MS', cursive, sans-serif; text-align: center; } .container { padding: 20px; } #upload { margin: 20px; } #map { height: 600px; margin: 20px auto; border: 3px solid #0f0; border-radius: 10px; box-shadow: 0 0 10px #0ff; } #play, .button { margin: 10px; padding: 10px 20px; background-color: #e91e63; color: white; border: none; border-radius: 5px; cursor: pointer; transition: 0.3s; } #play:hover, .button:hover { background-color: #ff5722; } .cyclist-icon { background: pink; border-radius: 50%; padding: 5px; color: #0ff; text-shadow: 0 0 5px #00f; } .stats-box { margin: 20px auto; padding: 20px; background: rgba(255, 255, 255, 0.1); border-radius: 10px; display: inline-block; min-width: 300px; } </style> </head> <body> <div class="container"> <h1>🔍 Cyberpunk Kawaii Cycle Analyser</h1> <input type="file" id="upload" accept=".csv"> <div id="controls" style="display:none;"> <button id="play">🚴♀️ Play Ride</button> <button class="button" onclick="toggleOverlay()">˟ Overlay</button> </div> <div id="sticker" style="margin:20px 0;"></div> <div id="stats"></div> <div id="map"></div> </div> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> <script> const map = L.map('map').setView([0, 0], 2); L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png', { attribution: '© CartoDB' }).addTo(map); document.getElementById('upload').addEventListener('change', handleFile, false); let dataPoints = [], latLngs = [], overlays = true; let heartRateLayers = []; function handleFile(event) { const file = event.target.files[0]; if (!file) return; map.eachLayer(l => { if (l instanceof L.Polyline && heartRateLayers.includes(l)) map.removeLayer(l); }); heartRateLayers = []; Papa.parse(file, { header: true, skipEmptyLines: true, complete: function(results) { processData(results.data); } }); } function processData(data) { const rawPoints = data .filter(row => row.latitude && row.longitude && row.distance && row.hr) .map(row => ({ ...row, latitude: parseFloat(row.latitude), longitude: parseFloat(row.longitude), hr: parseInt(row.hr), timestamp: parseFloat(row.timestamp), distance: parseFloat(row.distance), iso: row.ISO8601 })) .filter(row => !isNaN(row.latitude) && !isNaN(row.longitude)); dataPoints = []; let prevDist = -Infinity; rawPoints.forEach(row => { const dist = row.distance; if (dist > prevDist) { dataPoints.push(row); prevDist = dist; } }); if (dataPoints.length < 2) { alert("Needs more meaningful data!"); return; } document.getElementById('controls').style.display = 'block'; latLngs = dataPoints.map(p => [p.latitude, p.longitude]); map.fitBounds(latLngs); const totalDistance = (dataPoints[dataPoints.length-1].distance - dataPoints[0].distance) * 1000; // km to meters? const duration = (dataPoints[dataPoints.length-1].timestamp - dataPoints[0].timestamp) / 3600; const avgSpeed = (totalDistance / 1000) / duration; let maxSpeed = 0, totalHR = 0, maxHR = 0, hrCount = 0; const hrZones = { "Resting (<90)": 0, "Light (90-119)": 0, "Moderate (120-149)": 0, "Hard (150-179)": 0, "Max (180+)": 0 }; const hrKeys = Object.keys(hrZones); dataPoints.forEach((p, i) => { if (i === 0) return; const prev = dataPoints[i-1]; const deltaDist = p.distance - prev.distance; const deltaTime = (p.timestamp - prev.timestamp) / 3600; if (deltaTime > 0) { const speed = deltaDist / deltaTime; maxSpeed = Math.max(maxSpeed, speed * 3.6); } if (!isNaN(p.hr)) { totalHR += p.hr; maxHR = Math.max(maxHR, p.hr); hrCount++; let zone = "Max (180+)"; if (p.hr < 90) zone = "Resting (<90)"; else if (p.hr < 120) zone = "Light (90-119)"; else if (p.hr < 150) zone = "Moderate (120-149)"; else if (p.hr < 180) zone = "Hard (150-179)"; hrZones[zone]++; } }); const summary = { totalDistance: (totalDistance / 1000).toFixed(2), avgSpeed: avgSpeed.toFixed(2), maxSpeed: (maxSpeed || 0).toFixed(1), avgHR: (hrCount ? (totalHR / hrCount).toFixed(0) : 'N/A'), maxHR: maxHR, met: (1.2 + 0.04 * maxHR).toFixed(1) }; document.getElementById('stats').innerHTML = ` <div class="stats-box"> <h2>📊 Overview</h2> 🛣️ Total Distance: <b>${summary.totalDistance} km</b><br/> 🚴♂️ Avg/Max Speed: <b>${summary.avgSpeed} / ${summary.maxSpeed} km/h</b><br/> ❤️ Avg/Max HR: <b>${summary.avgHR}/${summary.maxHR} bpm</b><br/> 💥 Power: <b>${summary.met} METs</b> </div> `; // Plot heart rate zones on map for (let i=1; i<latLngs.length; i++) { const p = dataPoints[i]; if (p.hr && !isNaN(p.hr)) { const start = latLngs[i-1]; const end = latLngs[i]; const color = getHrColor(p.hr); const polyline = L.polyline([start, end], { color, weight: 4, opacity: 0.6 }).addTo(map); heartRateLayers.push(polyline); } } generateRewards(summary); } function getHrColor(hr) { if (hr < 90) return '#88f'; else if (hr < 120) return '#6f5'; else if (hr < 150) return '#ff0'; else if (hr < 180) return '#f60'; return '#f00'; } function toggleOverlay() { overlays = !overlays; heartRateLayers.forEach(l => map.removeLayer(l)); for (let i=1; i<latLngs.length; i++) { const p = dataPoints[i]; if (p.hr && !isNaN(p.hr)) { let color = '#ffc'; let weight = 3; if (!overlays) { color = getHrColor(p.hr); weight = 4; } const start = latLngs[i-1]; const end = latLngs[i]; const polyline = L.polyline([start, end], { color, weight: weight, opacity: 0.6 }).addTo(map); heartRateLayers.push(polyline); } } } function generateRewards(stats) { let sticker = ""; if (stats.avgHR > 150) sticker += 🎯 Passionate Energy: Burned 500+ Calories! `; if (stats.maxSpeed > 25) sticker += "⚡ Secret Trail Master (Speed Over 25 km/h)"; if (parseFloat(stats.totalDistance) > 10) sticker += "🌈 Distance Warrior (10+ km)"; document.getElementById('sticker').innerHTML = sticker || " "; } // Animated Playback const playBtn = document.getElementById('play'); let playbackInterval, currentIndex, marker, movingPolyline, movingPolylinePoints; playBtn.addEventListener('click', () => { if (!dataPoints.length) return; if (playbackInterval) { clearInterval(playbackInterval); playBtn.textContent = '🚴♀️ Play Ride'; return; } currentIndex = 0; movingPolylinePoints = []; movingPolyline = L.polyline([], { color: '#f0f', weight: 4 }).addTo(map); marker = L.marker([], { icon: L.divIcon({ className: 'cyclist-icon', html: '🚴' }) }).addTo(map); playBtn.textContent = '⏹️ Pause'; playbackInterval = setInterval(() => { if (currentIndex >= dataPoints.length) { clearInterval(playbackInterval); playBtn.textContent = '🚴♀️ Play Ride'; return; } const point = latLngs[currentIndex]; movingPolylinePoints.push(point); movingPolyline.setLatLngs(movingPolylinePoints); marker.setLatLng(point); map.panTo(point); currentIndex++; }, 50); }); </script> </body> </html> ```
# Cyberpunk Kawaii Cycle Ride Analyzer Here's a complete HTML implementation for your cyberpunk kawaii cycle ride analyzer with all the requested features: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cyber Kawaii Ride Analyzer</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <style> :root { --neon-pink: #ff6ec7; --neon-blue: #00f9ff; --neon-purple: #9d00ff; --dark-bg: #0a0a1a; --darker-bg: #050510; } body { background-color: var(--dark-bg); color: white; font-family: 'Courier New', monospace; margin: 0; padding: 0; overflow-x: hidden; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } header { text-align: center; margin-bottom: 30px; position: relative; } h1 { color: var(--neon-pink); font-size: 2.5rem; text-shadow: 0 0 10px var(--neon-pink); margin-bottom: 10px; } .subtitle { color: var(--neon-blue); font-size: 1.2rem; margin-bottom: 20px; } .upload-container { background-color: rgba(10, 10, 30, 0.7); border: 2px solid var(--neon-purple); border-radius: 10px; padding: 30px; margin-bottom: 30px; text-align: center; box-shadow: 0 0 20px rgba(157, 0, 255, 0.3); backdrop-filter: blur(5px); } .upload-btn { background: linear-gradient(45deg, var(--neon-purple), var(--neon-pink)); color: white; border: none; padding: 15px 30px; font-size: 1.2rem; border-radius: 50px; cursor: pointer; transition: all 0.3s; font-family: inherit; box-shadow: 0 0 15px rgba(255, 110, 199, 0.5); } .upload-btn:hover { transform: scale(1.05); box-shadow: 0 0 25px rgba(255, 110, 199, 0.8); } .stats-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; } .stat-card { background-color: rgba(5, 5, 20, 0.7); border: 1px solid var(--neon-blue); border-radius: 8px; padding: 20px; box-shadow: 0 0 10px rgba(0, 249, 255, 0.2); } .stat-title { color: var(--neon-blue); font-size: 1rem; margin-bottom: 10px; } .stat-value { font-size: 1.8rem; font-weight: bold; color: var(--neon-pink); } .map-container { height: 500px; margin-bottom: 30px; border: 2px solid var(--neon-purple); border-radius: 10px; overflow: hidden; position: relative; } #rideMap { height: 100%; background-color: var(--darker-bg); } .chart-container { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px; } .chart-card { background-color: rgba(5, 5, 20, 0.7); border: 1px solid var(--neon-purple); border-radius: 8px; padding: 20px; box-shadow: 0 0 10px rgba(157, 0, 255, 0.2); } .special-features { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px; } .feature-card { background-color: rgba(5, 5, 20, 0.7); border: 1px solid var(--neon-pink); border-radius: 8px; padding: 20px; box-shadow: 0 0 10px rgba(255, 110, 199, 0.2); } .feature-title { color: var(--neon-pink); font-size: 1.2rem; margin-bottom: 15px; } .kawaii-element { position: absolute; font-size: 2rem; opacity: 0.7; z-index: -1; } .glitch { animation: glitch 1s linear infinite; } @keyframes glitch { 0% { text-shadow: 0.05em 0 0 var(--neon-blue), -0.05em -0.025em 0 var(--neon-pink); } 14% { text-shadow: 0.05em 0 0 var(--neon-blue), -0.05em -0.025em 0 var(--neon-pink); } 15% { text-shadow: -0.05em -0.025em 0 var(--neon-blue), 0.025em 0.025em 0 var(--neon-pink); } 49% { text-shadow: -0.05em -0.025em 0 var(--neon-blue), 0.025em 0.025em 0 var(--neon-pink); } 50% { text-shadow: 0.025em 0.05em 0 var(--neon-blue), 0.05em 0 0 var(--neon-pink); } 99% { text-shadow: 0.025em 0.05em 0 var(--neon-blue), 0.05em 0 0 var(--neon-pink); } 100% { text-shadow: -0.025em 0 0 var(--neon-blue), -0.025em -0.025em 0 var(--neon-pink); } } .scanlines { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(rgba(0, 249, 255, 0.03) 1px, transparent 1px); background-size: 100% 3px; pointer-events: none; z-index: 1000; opacity: 0.5; } .corner-decoration { position: absolute; width: 50px; height: 50px; border-style: solid; border-color: var(--neon-blue); pointer-events: none; } .top-left { top: 10px; left: 10px; border-width: 2px 0 0 2px; } .top-right { top: 10px; right: 10px; border-width: 2px 2px 0 0; } .bottom-left { bottom: 10px; left: 10px; border-width: 0 0 2px 2px; } .bottom-right { bottom: 10px; right: 10px; border-width: 0 2px 2px 0; } .hidden { display: none; } .loading { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 1001; } .loading-spinner { border: 5px solid rgba(255, 110, 199, 0.3); border-radius: 50%; border-top: 5px solid var(--neon-pink); width: 50px; height: 50px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> </head> <body> <div class="scanlines"></div> <div class="corner-decoration top-left"></div> <div class="corner-decoration top-right"></div> <div class="corner-decoration bottom-left"></div> <div class="corner-decoration bottom-right"></div> <div class="kawaii-element" style="top: 10%; left: 5%;">(◕‿◕✿)</div> <div class="kawaii-element" style="top: 20%; right: 8%;">(ノ◕ヮ◕)ノ*:・゚✧</div> <div class="kawaii-element" style="bottom: 15%; left: 10%;">✧・゚: *✧・゚:*</div> <div class="kawaii-element" style="bottom: 25%; right: 5%;">(づ。◕‿‿◕。)づ</div> <div class="container"> <header> <h1 class="glitch">CYBER KAWAI RIDE ANALYZER</h1> <div class="subtitle">NEON-POWERED CYCLING PERFORMANCE METRICS</div> </header> <div class="upload-container"> <input type="file" id="fileInput" accept=".csv" style="display: none;"> <button class="upload-btn" id="uploadBtn">UPLOAD RIDE DATA CSV</button> <div id="fileName" style="margin-top: 10px; color: var(--neon-blue);"></div> </div> <div id="statsSection" class="hidden"> <h2 style="color: var(--neon-blue); margin-bottom: 20px;">RIDE STATISTICS</h2> <div class="stats-container" id="statsContainer"> <!-- Stats will be populated here --> </div> <h2 style="color: var(--neon-blue); margin-bottom: 20px;">RIDE MAP</h2> <div class="map-container"> <div id="rideMap"></div> </div> <h2 style="color: var(--neon-blue); margin-bottom: 20px;">PERFORMANCE METRICS</h2> <div class="chart-container"> <div class="chart-card"> <canvas id="hrChart"></canvas> </div> <div class="chart-card"> <canvas id="speedChart"></canvas> </div> </div> <h2 style="color: var(--neon-pink); margin-bottom: 20px;">SPECIAL FEATURES</h2> <div class="special-features"> <div class="feature-card"> <div class="feature-title">EMOTION WAVEFORM</div> <canvas id="emotionChart"></canvas> <div style="margin-top: 15px; font-size: 0.9rem;"> This shows how your emotional state might have fluctuated during the ride based on HR variability. </div> </div> <div class="feature-card"> <div class="feature-title">CYBER GHOST RIDER</div> <canvas id="ghostChart"></canvas> <div style="margin-top: 15px; font-size: 0.9rem;"> Compare your performance against a virtual rider with similar stats but optimized pacing. </div> </div> </div> </div> </div> <div id="loading" class="loading hidden"> <div class="loading-spinner"></div> <div style="color: var(--neon-pink); margin-top: 20px;">ANALYZING RIDE DATA...</div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const uploadBtn = document.getElementById('uploadBtn'); const fileInput = document.getElementById('fileInput'); const fileName = document.getElementById('fileName'); const statsSection = document.getElementById('statsSection'); const loading = document.getElementById('loading'); uploadBtn.addEventListener('click', function() { fileInput.click(); }); fileInput.addEventListener('change', function(e) { if (e.target.files.length) { fileName.textContent = `File selected: ${e.target.files[0].name}`; loading.classList.remove('hidden'); // Simulate processing delay setTimeout(() => { processFile(e.target.files[0]); loading.classList.add('hidden'); statsSection.classList.remove('hidden'); }, 2000); } }); function processFile(file) { const reader = new FileReader(); reader.onload = function(e) { const content = e.target.result; const lines = content.split('\n'); const headers = lines[0].split(','); const data = []; // Parse CSV data for (let i = 1; i < lines.length; i++) { if (lines[i].trim() === '') continue; const values = lines[i].split(','); const entry = {}; for (let j = 0; j < headers.length; j++) { entry[headers[j].trim()] = values[j] ? values[j].trim() : null; } // Only add entries with movement (distance > 0) if (entry.distance && parseFloat(entry.distance) > 0) { data.push(entry); } } // Calculate stats const stats = calculateStats(data); // Display stats displayStats(stats); // Initialize map initMap(data); // Create charts createCharts(data, stats); }; reader.readAsText(file); } function calculateStats(data) { if (data.length === 0) return {}; // Calculate basic stats let totalDistance = 0; let totalTime = 0; let maxSpeed = 0; let avgSpeed = 0; let maxHr = 0; let avgHr = 0; let totalElevationGain = 0; let previousElevation = parseFloat(data[0].elevation); const hrValues = []; const speedValues = []; for (let i = 1; i < data.length; i++) { const entry = data[i]; const prevEntry = data[i-1]; // Calculate time difference in hours const timeDiff = (parseFloat(entry.timestamp) - parseFloat(prevEntry.timestamp)) / 3600; totalTime += timeDiff; // Calculate distance const distance = parseFloat(entry.distance); totalDistance += distance; // Calculate speed in km/h const speed = distance / (timeDiff * 1000); speedValues.push(speed); if (speed > maxSpeed) maxSpeed = speed; // Calculate heart rate if (entry.hr) { const hr = parseFloat(entry.hr); hrValues.push(hr); if (hr > maxHr) maxHr = hr; } // Calculate elevation gain const currentElevation = parseFloat(entry.elevation); if (currentElevation > previousElevation) { totalElevationGain += currentElevation - previousElevation; } previousElevation = currentElevation; } // Calculate averages avgSpeed = totalDistance / (totalTime * 1000); avgHr = hrValues.length > 0 ? hrValues.reduce((a, b) => a + b, 0) / hrValues.length : 0; // Calculate METs (simplified estimation) const avgMets = avgHr > 0 ? (avgHr / 60) * 5 : 0; const maxMets = maxHr > 0 ? (maxHr / 60) * 5 : 0; return { totalDistance: (totalDistance / 1000).toFixed(2), totalTime: formatTime(totalTime), avgSpeed: avgSpeed.toFixed(2), maxSpeed: maxSpeed.toFixed(2), avgHr: Math.round(avgHr), maxHr: Math.round(maxHr), totalElevationGain: Math.round(totalElevationGain), avgMets: avgMets.toFixed(1), maxMets: maxMets.toFixed(1), intensityZones: calculateIntensityZones(hrValues) }; } function formatTime(hours) { const totalMinutes = hours * 60; const hrs = Math.floor(totalMinutes / 60); const mins = Math.floor(totalMinutes % 60); const secs = Math.floor((totalMinutes * 60) % 60); return `${hrs}h ${mins}m ${secs}s`; } function calculateIntensityZones(hrValues) { if (hrValues.length === 0) return {}; const maxHr = Math.max(...hrValues); const zones = { zone1: 0, // 50-60% of max HR zone2: 0, // 60-70% zone3: 0, // 70-80% zone4: 0, // 80-90% zone5: 0 // 90-100% }; hrValues.forEach(hr => { const percentage = (hr / maxHr) * 100; if (percentage < 60) zones.zone1++; else if (percentage < 70) zones.zone2++; else if (percentage < 80) zones.zone3++; else if (percentage < 90) zones.zone4++; else zones.zone5++; }); return zones; } function displayStats(stats) { const statsContainer = document.getElementById('statsContainer'); statsContainer.innerHTML = ''; const statCards = [ { title: 'TOTAL DISTANCE', value: `${stats.totalDistance} km`, icon: '🚴♀️' }, { title: 'TOTAL TIME', value: stats.totalTime, icon: '⏱️' }, { title: 'AVG SPEED', value: `${stats.avgSpeed} km/h`, icon: '📊' }, { title: 'MAX SPEED', value: `${stats.maxSpeed} km/h`, icon: '💨' }, { title: 'AVG HEART RATE', value: `${stats.avgHr} bpm`, icon: '❤️' }, { title: 'MAX HEART RATE', value: `${stats.maxHr} bpm`, icon: '🔥' }, { title: 'ELEVATION GAIN', value: `${stats.totalElevationGain} m`, icon: '⛰️' }, { title: 'AVG METS', value: stats.avgMets, icon: '⚡' }, { title: 'MAX METS', value: stats.maxMets, icon: '☄️' }, { title: 'INTENSITY ZONE 1', value: `${((stats.intensityZones.zone1 / (stats.intensityZones.zone1 + stats.intensityZones.zone2 + stats.intensityZones.zone3 + stats.intensityZones.zone4 + stats.intensityZones.zone5)) * 100).toFixed(1)}%`, icon: '😌' }, { title: 'INTENSITY ZONE 5', value: `${((stats.intensityZones.zone5 / (stats.intensityZones.zone1 + stats.intensityZones.zone2 + stats.intensityZones.zone3 + stats.intensityZones.zone4 + stats.intensityZones.zone5)) * 100).toFixed(1)}%`, icon: '🤯' } ]; statCards.forEach(stat => { const card = document.createElement('div'); card.className = 'stat-card'; card.innerHTML = ` <div class="stat-title">${stat.icon} ${stat.title}</div> <div class="stat-value">${stat.value}</div> `; statsContainer.appendChild(card); }); } function initMap(data) { if (data.length === 0 || !data[0].latitude || !data[0].longitude) return; // Get first and last coordinates for bounds const firstCoord = [parseFloat(data[0].latitude), parseFloat(data[0].longitude)]; const lastCoord = [parseFloat(data[data.length-1].latitude), parseFloat(data[data.length-1].longitude)]; // Initialize map const map = L.map('rideMap').setView(firstCoord, 15); // Add tile layer (using a cyberpunk-style map) L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', maxZoom: 19 }).addTo(map); // Fit bounds to include all points const bounds = L.latLngBounds([firstCoord, lastCoord]); map.fitBounds(bounds); // Create polyline with color based on heart rate const latLngs = []; const hrValues = []; data.forEach(entry => { if (entry.latitude && entry.longitude) { latLngs.push([parseFloat(entry.latitude), parseFloat(entry.longitude)]); hrValues.push(entry.hr ? parseFloat(entry.hr) : 0); } }); // Find min and max HR for color scaling const maxHr = Math.max(...hrValues.filter(hr => hr > 0)); const minHr = Math.min(...hrValues.filter(hr => hr > 0)); // Create segments with different colors for (let i = 1; i < latLngs.length; i++) { const hr = hrValues[i] || hrValues[i-1] || minHr; const intensity = (hr - minHr) / (maxHr - minHr); let color; if (intensity < 0.2) color = '#00ffff'; // Light blue else if (intensity < 0.4) color = '#00ff88'; // Teal else if (intensity < 0.6) color = '#ffff00'; // Yellow else if (intensity < 0.8) color = '#ff8800'; // Orange else color = '#ff0000'; // Red L.polyline([latLngs[i-1], latLngs[i]], { color: color, weight: 5, opacity: 0.8, lineJoin: 'round' }).addTo(map); } // Add start and end markers L.marker(firstCoord, { icon: L.divIcon({ className: 'start-marker', html: '✧ START ✧', iconSize: [100, 20] }) }).addTo(map); L.marker(lastCoord, { icon: L.divIcon({ className: 'end-marker', html: '✧ END ✧', iconSize: [100, 20] }) }).addTo(map); } function createCharts(data, stats) { // Prepare data for charts const timestamps = []; const hrData = []; const speedData = []; const distanceData = []; const elevationData = []; let cumulativeDistance = 0; data.forEach((entry, index) => { if (entry.ISO8601) timestamps.push(entry.ISO8601.split('T')[1].substring(0, 8)); else timestamps.push(index.toString()); hrData.push(entry.hr ? parseFloat(entry.hr) : null); if (index > 0) { const timeDiff = (parseFloat(entry.timestamp) - parseFloat(data[index-1].timestamp)) / 3600; const distance = parseFloat(entry.distance); cumulativeDistance += distance; const speed = distance / (timeDiff * 1000); speedData.push(speed); } else { speedData.push(0); } distanceData.push(cumulativeDistance); elevationData.push(entry.elevation ? parseFloat(entry.elevation) : null); }); // Heart Rate Chart const hrCtx = document.getElementById('hrChart').getContext('2d'); new Chart(hrCtx, { type: 'line', data: { labels: timestamps, datasets: [{ label: 'Heart Rate (bpm)', data: hrData, borderColor: '#ff6ec7', backgroundColor: 'rgba(255, 110, 199, 0.1)', borderWidth: 2, tension: 0.1, fill: true }] }, options: { responsive: true, plugins: { legend: { labels: { color: '#ffffff' } } }, scales: { x: { ticks: { color: '#00f9ff' }, grid: { color: 'rgba(0, 249, 255, 0.1)' } }, y: { ticks: { color: '#00f9ff' }, grid: { color: 'rgba(0, 249, 255, 0.1)' } } } } }); // Speed Chart const speedCtx = document.getElementById('speedChart').getContext('2d'); new Chart(speedCtx, { type: 'line', data: { labels: timestamps, datasets: [{ label: 'Speed (km/h)', data: speedData, borderColor: '#00f9ff', backgroundColor: 'rgba(0, 249, 255, 0.1)', borderWidth: 2, tension: 0.1, fill: true }] }, options: { responsive: true, plugins: { legend: { labels: { color: '#ffffff' } } }, scales: { x: { ticks: { color: '#00f9ff' }, grid: { color: 'rgba(0, 249, 255, 0.1)' } }, y: { ticks: { color: '#00f9ff' }, grid: { color: 'rgba(0, 249, 255, 0.1)' } } } } }); // Emotion Waveform Chart (special feature 1) const emotionCtx = document.getElementById('emotionChart').getContext('2d'); // Generate synthetic emotion data based on HR variability const emotionData = []; let prevHr = hrData[0] || 0; hrData.forEach(hr => { if (hr === null) { emotionData.push(null); return; } const hrChange = hr - prevHr; let emotionValue; if (hrChange > 5) emotionValue = 1; // Excited else if (hrChange < -5) emotionValue = -1; // Tired else emotionValue = Math.sin(Date.now() / 1000 + Math.random()) * 0.5; // Neutral with variation emotionData.push(emotionValue); prevHr = hr; }); new Chart(emotionCtx, { type: 'line', data: { labels: timestamps, datasets: [{ label: 'Emotion Waveform', data: emotionData, borderColor: '#9d00ff', backgroundColor: 'rgba(157, 0, 255, 0.1)', borderWidth: 2, tension: 0.3, fill: true }] }, options: { responsive: true, plugins: { legend: { labels: { color: '#ffffff' } } }, scales: { x: { ticks: { color: '#00f9ff' }, grid: { color: 'rgba(0, 249, 255, 0.1)' } }, y: { min: -1.5, max: 1.5, ticks: { color: '#00f9ff', callback: function(value) { if (value === -1) return '😴 Tired'; if (value === 0) return '😐 Neutral'; if (value === 1) return '🤩 Excited'; return ''; } }, grid: { color: 'rgba(0, 249, 255, 0.1)' } } } } }); // Cyber Ghost Rider Chart (special feature 2) const ghostCtx = document.getElementById('ghostChart').getContext('2d'); // Generate optimized speed data const ghostSpeedData = []; const avgSpeed = parseFloat(stats.avgSpeed); speedData.forEach((speed, index) => { // Ghost rider maintains more consistent speed with occasional bursts const variation = Math.sin(index / 10) * 0.2 * avgSpeed; const burst = Math.random() < 0.1 ? avgSpeed * 0.3 : 0; ghostSpeedData.push(avgSpeed + variation + burst); }); new Chart(ghostCtx, { type: 'line', data: { labels: timestamps, datasets: [ { label: 'Your Speed', data: speedData, borderColor: '#ff6ec7', backgroundColor: 'rgba(255, 110, 199, 0.1)', borderWidth: 1, tension: 0.1 }, { label: 'Ghost Rider Speed', data: ghostSpeedData, borderColor: '#00f9ff', backgroundColor: 'rgba(0, 249, 255, 0.1)', borderWidth: 1, tension: 0.1, borderDash: [5, 5] } ] }, options: { responsive: true, plugins: { legend: { labels: { color: '#ffffff' } } }, scales: { x: { ticks: { color: '#00f9ff' }, grid: { color: 'rgba(0, 249, 255, 0.1)' } }, y: { ticks: { color: '#00f9ff' }, grid: { color: 'rgba(0, 249, 255, 0.1)' } } } } }); } }); </script> </body> </html> ``` ## Features Included 1. **CSV File Processing**: - Upload button for CSV files - Parses and filters out stationary periods - Calculates comprehensive ride statistics 2. **Cyberpunk Kawaii UI**: - Neon color scheme with glitch effects - Kawaii decorative elements (cute faces and sparkles) - Scanlines and corner decorations for retro-futuristic feel 3. **Ride Statistics Display**: - Distance, time, speed, heart rate, elevation, and METS - Intensity zone breakdown - Visually appealing stat cards 4. **Interactive Map**: - Displays ride path with color-coded segments based on heart rate intensity - Start and end markers - Uses a dark basemap for cyberpunk aesthetic 5. **Performance Charts**: - Heart rate over time - Speed over time - Interactive and responsive 6. **Special Features**: - **Emotion Waveform**: Visualizes estimated emotional state during ride based on HR variability - **Cyber Ghost Rider**: Compares your performance against a virtual rider with optimized pacing The app fully processes the CSV data as specified, filtering out stationary periods and providing comprehensive analysis with a unique cyberpunk kawaii aesthetic.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cyberpunk Kawaii Cycle Ride Analyser</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Leaflet CSS --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" integrity="sha512-sA+eJd6k2fI6xk+UiH2u6H+0m0D6iG+2ue6G6jHgqRsnYlL/xODzp2txf9JAv1Hhn67+IZPm5qX5q3w8nYlipQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <!-- Google Font --> <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet"> <style> body { margin: 0; font-family: 'Press Start 2P', cursive; background: #0a0a0f; color: #eee; display: flex; flex-direction: column; align-items: center; } h1 { margin: 20px; color: #f72585; text-shadow: 0 0 8px #f72585, 0 0 16px #7209b7; } #uploader { margin: 10px; } #stats, #extras { margin: 10px; padding: 10px; background: rgba(20,20,30,0.8); border: 2px solid #3a0ca3; border-radius: 8px; max-width: 800px; width: 90%; } #map { width: 90%; height: 400px; margin: 10px; border: 2px solid #4361ee; border-radius: 8px; } #chartContainer { width: 90%; max-width: 800px; margin: 10px; } .btn { display: inline-block; margin: 5px; padding: 10px 20px; background: #4361ee; color: #fff; text-decoration: none; border-radius: 4px; cursor: pointer; box-shadow: 0 0 8px #4361ee; transition: 0.2s; } .btn:hover { background: #4895ef; box-shadow: 0 0 12px #4895ef; } .kawaiiMarker { font-size: 32px; text-shadow: 0 0 4px #ff006e, 0 0 8px #fb5607; } .tree { display: inline-block; margin: 0 2px; font-size: 24px; } </style> </head> <body> <h1>🌸 Cyberpunk Kawaii Cycle Ride Analyser 🌸</h1> <input type="file" id="uploader" accept=".csv"> <div id="stats" style="display:none;"></div> <div id="map" style="display:none;"></div> <div id="chartContainer" style="display:none;"> <canvas id="rideChart"></canvas> </div> <div id="extras" style="display:none;"></div> <!-- Leaflet JS --> <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js" integrity="sha512-o9QIdf7RBdlx6C+tdbK1LGmGSMSHtyE7qaNvRw7N5ptr2+R+LMQIphGhLh20p5S/u5zha/7JKrSAZEQmXh6DKw==" crossorigin="anonymous" referrerpolicy="no-referrer" ></script> <!-- Chart.js --> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> const uploader = document.getElementById('uploader'); const statsDiv = document.getElementById('stats'); const mapDiv = document.getElementById('map'); const chartContainer = document.getElementById('chartContainer'); const extrasDiv = document.getElementById('extras'); let map, rideLayer, marker, rideChart; uploader.addEventListener('change', handleFile, false); function handleFile(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { const text = e.target.result; const raw = parseCSV(text); const segments = processData(raw); displayStats(segments); initMap(); drawPath(segments); initChart(segments); showExtras(segments); }; reader.readAsText(file); } function parseCSV(text) { const lines = text.trim().split('\n'); const data = []; for (let i = 1; i < lines.length; i++) { const cols = lines[i].split(','); const [time, ts, iso, hr, lat, lng, elev, dist] = cols; data.push({ time, timestamp: parseFloat(ts), hr: hr ? +hr : null, lat: lat ? +lat : null, lng: lng ? +lng : null, elev: elev ? +elev : null, dist: dist ? +dist : null }); } return data; } function processData(points) { const segments = []; for (let i = 1; i < points.length; i++) { const a = points[i-1], b = points[i]; if (a.lat==null||b.lat==null) continue; const dd = b.dist - a.dist; const dt = (b.timestamp - a.timestamp); if (dd <= 0 || dt <= 0) continue; const speed_kmh = dd * 3600 / dt; const elevDiff = b.elev - a.elev; const grade = dd>0 ? (elevDiff/(dd*1000)) : 0; const speed_m_min = speed_kmh * 1000 / 60; const vo2 = 0.1*speed_m_min + 1.8*speed_m_min*grade + 3.5; const mets = vo2/3.5; segments.push({ from: a, to: b, speed: speed_kmh, hr: b.hr, mets, dt }); } return segments; } function displayStats(segs) { const speeds = segs.map(s=>s.speed); const hrs = segs.map(s=>s.hr).filter(v=>v!=null); const mets = segs.map(s=>s.mets); const totalDist = segs.reduce((sum,s)=>sum + (s.from.dist?0:0) + (s.to.dist - s.from.dist),0); const totalTime = segs.reduce((sum,s)=>sum + s.dt,0)/3600; // h const maxSpeed = Math.max(...speeds).toFixed(1); const avgSpeed = (speeds.reduce((a,b)=>a+b,0)/speeds.length).toFixed(1); const maxHr = Math.max(...hrs).toFixed(0); const avgHr = (hrs.reduce((a,b)=>a+b,0)/hrs.length).toFixed(0); const avgMets = (mets.reduce((a,b)=>a+b,0)/mets.length).toFixed(1); statsDiv.innerHTML = ` <h2>📝 Overview Statistics</h2> <p>Highest Speed: <strong>${maxSpeed} km/h</strong></p> <p>Average Speed: <strong>${avgSpeed} km/h</strong></p> <p>Highest Heart Rate: <strong>${maxHr} bpm</strong></p> <p>Average Heart Rate: <strong>${avgHr} bpm</strong></p> <p>Average METS: <strong>${avgMets}</strong></p> `; statsDiv.style.display = 'block'; } function initMap() { if (map) map.remove(); map = L.map('map').setView([0,0], 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:''}).addTo(map); rideLayer = L.layerGroup().addTo(map); mapDiv.style.display = 'block'; } function drawPath(segs) { const lats = segs.map(s=>s.to.lat); const lngs = segs.map(s=>s.to.lng); const avgLat = lats.reduce((a,b)=>a+b)/lats.length; const avgLng = lngs.reduce((a,b)=>a+b)/lngs.length; map.setView([avgLat, avgLng], 15); // draw colored segments segs.forEach(s => { const c = hrColor(s.hr); L.polyline([[s.from.lat, s.from.lng],[s.to.lat, s.to.lng]], {color: c, weight: 5, opacity:0.8}).addTo(rideLayer); }); // prepare marker marker = L.marker([segs[0].from.lat, segs[0].from.lng], { icon: L.divIcon({className:'kawaiiMarker', html:'🚴'}) }).addTo(map); // playback button const btn = document.createElement('button'); btn.innerText = "▶️ Play Ride Animation"; btn.className = 'btn'; btn.onclick = ()=>animateRide(segs); mapDiv.appendChild(btn); } function hrColor(hr) { if (hr==null) return '#888'; const pct = Math.min(Math.max((hr-70)/100,0),1); const r = Math.floor(255*pct); const g = Math.floor(255*(1-pct)); return `rgb(${r},${g},50)`; } function initChart(segs) { const labels = segs.map(s=>s.to.time); const dataSpeed = segs.map(s=>s.speed.toFixed(1)); const dataHr = segs.map(s=>s.hr); const dataMets = segs.map(s=>s.mets.toFixed(1)); const ctx = document.getElementById('rideChart').getContext('2d'); if (rideChart) rideChart.destroy(); rideChart = new Chart(ctx, { type: 'line', data: { labels, datasets: [ {label:'Speed (km/h)', data:dataSpeed, borderColor:'#4cc9f0', fill:false, tension:0.2}, {label:'Heart Rate (bpm)', data:dataHr, borderColor:'#f72585', fill:false, tension:0.2}, {label:'METS', data:dataMets, borderColor:'#fee440', fill:false, tension:0.2} ] }, options:{ responsive:true, plugins:{legend:{position:'bottom'}}, scales:{ x:{display:false}, y:{beginAtZero:true} } } }); chartContainer.style.display='block'; } function animateRide(segs) { let idx = 0; function step() { if (idx >= segs.length) return; const s = segs[idx]; marker.setLatLng([s.to.lat, s.to.lng]); idx++; setTimeout(step, s.dt * 0.5); // speed up 2x } step(); } function showExtras(segs) { // Feature 1: Carbon footprint saved const totalDist = segs.reduce((sum,s)=>sum + (s.to.dist - s.from.dist),0); const co2 = (totalDist * 0.12).toFixed(2); // kg CO2 saved const trees = Math.floor(co2 / 0.8); let treeIcons = ''; for (let i=0;i<trees;i++) treeIcons += '<span class="tree">🌳</span>'; // Feature 2: Kawaii Avatar Mood const avgHr = segs.map(s=>s.hr).filter(v=>v).reduce((a,b)=>a+b)/segs.filter(s=>s.hr).length; let mood = '😊'; if (avgHr > 160) mood = '😅'; else if (avgHr < 100) mood = '😌'; extrasDiv.innerHTML = ` <h2>✨ Additional Features</h2> <p>🍃 Carbon Saved: <strong>${co2} kg CO₂</strong> (${trees} trees planted!)<br>${treeIcons}</p> <p>🥰 Your Kawaii Avatar Mood: <span style="font-size:32px;">${mood}</span></p> `; extrasDiv.style.display='block'; } </script> </body> </html> ```
# Cyberpunk Kawaii Cycle Ride Analyser I'll create a web app that analyzes cycling data with a cyberpunk kawaii aesthetic. The app will process CSV files, discard stationary sections, display statistics, visualize the route with heart rate coloring, and include two unique features: an animated replay and a terrain difficulty analysis. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cyberpunk Kawaii Cycle Ride Analyser</title> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> <style> :root { --neon-pink: #ff00ff; --neon-blue: #00ffff; --neon-green: #00ff00; --neon-purple: #9d00ff; --dark-bg: #0f0f23; --card-bg: rgba(25, 25, 50, 0.7); } * { margin: 0; padding: 0; box-sizing: border-box; } body { background-color: var(--dark-bg); color: #fff; font-family: 'Poppins', sans-serif; background-image: radial-gradient(circle at 10% 20%, rgba(255, 0, 255, 0.1) 0%, transparent 20%), radial-gradient(circle at 90% 80%, rgba(0, 255, 255, 0.1) 0%, transparent 20%); min-height: 100vh; padding: 20px; position: relative; overflow-x: hidden; } body::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(0deg, transparent 24%, rgba(15, 15, 35, 0.5) 25%, rgba(15, 15, 35, 0.5) 26%, transparent 27%), linear-gradient(90deg, transparent 24%, rgba(15, 15, 35, 0.5) 25%, rgba(15, 15, 35, 0.5) 26%, transparent 27%); background-size: 50px 50px; z-index: -1; opacity: 0.3; } .glow { text-shadow: 0 0 5px var(--neon-pink), 0 0 10px var(--neon-pink), 0 0 20px var(--neon-pink); } .container { max-width: 1400px; margin: 0 auto; } header { text-align: center; padding: 20px 0; margin-bottom: 30px; position: relative; } h1 { font-family: 'Orbitron', sans-serif; font-size: 3.5rem; color: var(--neon-pink); margin-bottom: 10px; letter-spacing: 3px; } .subtitle { font-size: 1.2rem; color: var(--neon-blue); max-width: 600px; margin: 0 auto; line-height: 1.6; } .kawaii { color: #ff9ec8; font-weight: bold; text-shadow: 0 0 10px rgba(255, 158, 200, 0.7); } .upload-section { background: var(--card-bg); border: 1px solid var(--neon-purple); border-radius: 15px; padding: 30px; margin-bottom: 30px; box-shadow: 0 0 20px rgba(157, 0, 255, 0.3); text-align: center; } .upload-area { border: 2px dashed var(--neon-green); border-radius: 10px; padding: 40px 20px; margin: 20px 0; cursor: pointer; transition: all 0.3s ease; background: rgba(0, 0, 0, 0.2); } .upload-area:hover { background: rgba(0, 255, 255, 0.1); border-color: var(--neon-blue); } .upload-area p { color: var(--neon-green); font-size: 1.2rem; margin-bottom: 20px; } .btn { background: linear-gradient(45deg, var(--neon-pink), var(--neon-purple)); color: white; border: none; padding: 12px 30px; border-radius: 50px; font-family: 'Orbitron', sans-serif; font-size: 1.1rem; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 0 15px rgba(255, 0, 255, 0.5); display: inline-block; margin-top: 10px; } .btn:hover { transform: translateY(-3px); box-shadow: 0 0 20px rgba(255, 0, 255, 0.8); } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; } .stat-card { background: var(--card-bg); border: 1px solid var(--neon-blue); border-radius: 15px; padding: 20px; text-align: center; box-shadow: 0 0 15px rgba(0, 255, 255, 0.2); transition: transform 0.3s ease; } .stat-card:hover { transform: translateY(-5px); } .stat-card h3 { color: var(--neon-blue); margin-bottom: 15px; font-size: 1.2rem; } .stat-value { font-size: 2.5rem; font-family: 'Orbitron', sans-serif; color: var(--neon-green); margin: 10px 0; } .stat-unit { color: #aaa; font-size: 0.9rem; } .map-container { height: 500px; background: var(--card-bg); border: 1px solid var(--neon-green); border-radius: 15px; margin-bottom: 30px; overflow: hidden; box-shadow: 0 0 20px rgba(0, 255, 0, 0.2); position: relative; } #map { height: 100%; width: 100%; border-radius: 15px; } .chart-container { background: var(--card-bg); border: 1px solid var(--neon-purple); border-radius: 15px; padding: 20px; margin-bottom: 30px; box-shadow: 0 0 20px rgba(157, 0, 255, 0.3); } .chart-title { text-align: center; color: var(--neon-pink); margin-bottom: 20px; font-family: 'Orbitron', sans-serif; } .features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; margin-bottom: 30px; } .feature-card { background: var(--card-bg); border: 1px solid var(--neon-blue); border-radius: 15px; padding: 20px; box-shadow: 0 0 15px rgba(0, 255, 255, 0.2); } .feature-card h3 { color: var(--neon-green); margin-bottom: 15px; display: flex; align-items: center; } .feature-card h3 i { margin-right: 10px; color: var(--neon-pink); } .controls { display: flex; justify-content: center; gap: 15px; margin-top: 20px; } .control-btn { background: rgba(0, 255, 255, 0.2); color: var(--neon-blue); border: 1px solid var(--neon-blue); padding: 10px 20px; border-radius: 50px; cursor: pointer; transition: all 0.3s ease; } .control-btn:hover { background: rgba(0, 255, 255, 0.4); } .hidden { display: none; } .legend { background: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 5px; position: absolute; bottom: 20px; right: 20px; z-index: 1000; border: 1px solid var(--neon-purple); } .legend-item { display: flex; align-items: center; margin: 5px 0; } .legend-color { width: 20px; height: 10px; margin-right: 10px; border-radius: 2px; } footer { text-align: center; padding: 20px; color: #aaa; font-size: 0.9rem; margin-top: 30px; border-top: 1px solid rgba(255, 255, 255, 0.1); } canvas { background: rgba(0, 0, 0, 0.2); border-radius: 10px; } .bike-icon { position: absolute; font-size: 24px; z-index: 1000; text-shadow: 0 0 10px var(--neon-pink); color: var(--neon-pink); transform: translate(-50%, -50%); } .terrain-chart { height: 300px; margin-top: 20px; } .loading { display: flex; justify-content: center; align-items: center; height: 200px; color: var(--neon-blue); font-size: 1.2rem; } .loading::after { content: "•"; animation: dots 1.5s infinite; } @keyframes dots { 0%, 20% { content: "•"; } 40% { content: "••"; } 60% { content: "•••"; } 80%, 100% { content: "••••"; } } @keyframes glowPulse { 0% { box-shadow: 0 0 5px var(--neon-pink); } 50% { box-shadow: 0 0 20px var(--neon-pink); } 100% { box-shadow: 0 0 5px var(--neon-pink); } } .pulse { animation: glowPulse 2s infinite; } </style> </head> <body> <div class="container"> <header> <h1 class="glow">CYBERPUNK <span class="kawaii">KAWAII</span> RIDE ANALYSER</h1> <p class="subtitle">Upload your cycling data CSV to visualize your ride with neon aesthetics and advanced analytics</p> </header> <section class="upload-section"> <h2>Upload Your Ride Data</h2> <div class="upload-area" id="dropZone"> <p>Drag & drop your CSV file here or click to browse</p> <input type="file" id="fileInput" accept=".csv" class="hidden"> <button class="btn" id="browseBtn">Browse Files</button> </div> <p>CSV format: date,timestamp,ISO8601,hr,latitude,longitude,elevation,distance,lap,since_start</p> </section> <div id="statsSection" class="hidden"> <div class="stats-grid"> <div class="stat-card"> <h3>Highest Speed</h3> <div class="stat-value">42.6<span class="stat-unit">km/h</span></div> </div> <div class="stat-card"> <h3>Average Speed</h3> <div class="stat-value">28.3<span class="stat-unit">km/h</span></div> </div> <div class="stat-card"> <h3>Max Heart Rate</h3> <div class="stat-value">172<span class="stat-unit">bpm</span></div> </div> <div class="stat-card"> <h3>Avg Heart Rate</h3> <div class="stat-value">148<span class="stat-unit">bpm</span></div> </div> <div class="stat-card"> <h3>Total Distance</h3> <div class="stat-value">24.7<span class="stat-unit">km</span></div> </div> <div class="stat-card"> <h3>Total Ascent</h3> <div class="stat-value">384<span class="stat-unit">m</span></div> </div> <div class="stat-card"> <h3>Calories Burned</h3> <div class="stat-value">612<span class="stat-unit">kcal</span></div> </div> <div class="stat-card"> <h3>METs</h3> <div class="stat-value">8.2<span class="stat-unit">avg</span></div> </div> </div> <div class="map-container"> <div id="map"></div> <div class="legend"> <h4>Heart Rate Zones</h4> <div class="legend-item"> <div class="legend-color" style="background: #00ff00;"></div> <span>Low (50-100 bpm)</span> </div> <div class="legend-item"> <div class="legend-color" style="background: #ffff00;"></div> <span>Moderate (100-130 bpm)</span> </div> <div class="legend-item"> <div class="legend-color" style="background: #ffa500;"></div> <span>High (130-160 bpm)</span> </div> <div class="legend-item"> <div class="legend-color" style="background: #ff0000;"></div> <span>Max (160+ bpm)</span> </div> </div> </div> <div class="chart-container"> <h3 class="chart-title">RIDE METRICS OVER TIME</h3> <canvas id="metricsChart"></canvas> </div> <div class="features-grid"> <div class="feature-card"> <h3><i>🚴</i> Ride Animation Replay</h3> <p>Watch an animated replay of your ride with a cute cyberpunk bike icon moving along your route. The animation shows your speed and heart rate at each point.</p> <div class="controls"> <button class="control-btn" id="playBtn">▶ Play</button> <button class="control-btn" id="pauseBtn">⏸ Pause</button> <button class="control-btn" id="resetBtn">↺ Reset</button> <input type="range" id="speedControl" min="1" max="10" value="5"> </div> </div> <div class="feature-card"> <h3><i>⛰️</i> Terrain Difficulty Analysis</h3> <p>See how different terrain types affected your heart rate and effort. Our algorithm categorizes terrain and shows performance metrics for each type.</p> <canvas class="terrain-chart" id="terrainChart"></canvas> </div> </div> </div> <div id="loadingSection" class="loading hidden"> Analysing your ride data... </div> <footer> <p>Cyberpunk Kawaii Cycle Ride Analyser • Made with 💖 and neon lights</p> </footer> </div> <script> // Initialize map const map = L.map('map').setView([51.505, -0.09], 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', maxZoom: 18 }).addTo(map); // Add cyberpunk-themed map layer const cyberTile = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', { maxZoom: 20, attribution: '© <a href="https://stadiamaps.com/">Stadia Maps</a>' }).addTo(map); // Add glow effect to map const mapContainer = document.querySelector('.map-container'); mapContainer.classList.add('pulse'); // File upload handling const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const browseBtn = document.getElementById('browseBtn'); const statsSection = document.getElementById('statsSection'); const loadingSection = document.getElementById('loadingSection'); browseBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', handleFileUpload); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.borderColor = '#00ffff'; dropZone.style.backgroundColor = 'rgba(0, 255, 255, 0.2)'; }); dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = '#00ff00'; dropZone.style.backgroundColor = 'rgba(0, 0, 0, 0.2)'; }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.borderColor = '#00ff00'; dropZone.style.backgroundColor = 'rgba(0, 0, 0, 0.2)'; if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; handleFileUpload(); } }); function handleFileUpload() { if (fileInput.files.length === 0) return; const file = fileInput.files[0]; if (file.type !== 'text/csv' && !file.name.endsWith('.csv')) { alert('
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>🌸 CyberRide Analyst 🚴♀️</title> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Noto+Color+Emoji&display=swap" rel="stylesheet"> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Orbitron', monospace; background: linear-gradient(135deg, #0f0f23 0%, #1a1a40 50%, #2d1b69 100%); color: #fff; min-height: 100vh; overflow-x: hidden; } .neon-text { text-shadow: 0 0 5px #ff6b9d, 0 0 10px #ff6b9d, 0 0 15px #ff6b9d, 0 0 20px #ff6b9d; } .cyber-container { max-width: 1400px; margin: 0 auto; padding: 20px; } .header { text-align: center; margin-bottom: 30px; background: linear-gradient(45deg, #ff6b9d, #4ecdc4); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .upload-zone { border: 3px dashed #4ecdc4; border-radius: 15px; padding: 40px; text-align: center; margin-bottom: 30px; background: rgba(78, 205, 196, 0.1); transition: all 0.3s ease; position: relative; overflow: hidden; } .upload-zone::before { content: ''; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; background: linear-gradient(45deg, #ff6b9d, #4ecdc4, #ff6b9d); border-radius: 15px; z-index: -1; animation: glow 2s ease-in-out infinite alternate; } @keyframes glow { from { opacity: 0.5; } to { opacity: 1; } } .upload-zone:hover { border-color: #ff6b9d; background: rgba(255, 107, 157, 0.1); } .file-input { display: none; } .upload-btn { background: linear-gradient(45deg, #ff6b9d, #4ecdc4); border: none; padding: 15px 30px; border-radius: 25px; color: #fff; font-family: 'Orbitron', monospace; font-weight: bold; cursor: pointer; font-size: 16px; transition: all 0.3s ease; box-shadow: 0 5px 15px rgba(255, 107, 157, 0.4); } .upload-btn:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(255, 107, 157, 0.6); } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; } .stat-card { background: rgba(255, 255, 255, 0.05); border: 1px solid #4ecdc4; border-radius: 15px; padding: 20px; backdrop-filter: blur(10px); position: relative; overflow: hidden; } .stat-card::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 2px; background: linear-gradient(90deg, transparent, #ff6b9d, transparent); animation: scan 3s infinite; } @keyframes scan { 0% { left: -100%; } 100% { left: 100%; } } .stat-title { color: #4ecdc4; font-size: 14px; margin-bottom: 5px; display: flex; align-items: center; gap: 8px; } .stat-value { font-size: 28px; font-weight: bold; color: #ff6b9d; text-shadow: 0 0 10px rgba(255, 107, 157, 0.5); } .map-container { height: 500px; border-radius: 15px; overflow: hidden; margin-bottom: 30px; border: 2px solid #4ecdc4; box-shadow: 0 10px 30px rgba(78, 205, 196, 0.3); } .analysis-tabs { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; } .tab-btn { background: rgba(255, 255, 255, 0.1); border: 1px solid #4ecdc4; color: #4ecdc4; padding: 10px 20px; border-radius: 20px; cursor: pointer; font-family: 'Orbitron', monospace; transition: all 0.3s ease; } .tab-btn.active, .tab-btn:hover { background: #4ecdc4; color: #0f0f23; box-shadow: 0 5px 15px rgba(78, 205, 196, 0.4); } .chart-container { background: rgba(255, 255, 255, 0.05); border-radius: 15px; padding: 20px; margin-bottom: 20px; border: 1px solid #4ecdc4; } .hidden { display: none !important; } .kawaii-emoji { font-size: 24px; margin-right: 10px; } .loading { display: none; text-align: center; color: #4ecdc4; margin: 20px 0; } .loading.show { display: block; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .loading { animation: pulse 1.5s infinite; } .achievement-badge { display: inline-block; padding: 5px 10px; border-radius: 15px; font-size: 12px; margin: 2px; background: linear-gradient(45deg, #ffd700, #ffed4e); color: #0f0f23; font-weight: bold; } .power-zone { display: flex; align-items: center; gap: 10px; margin: 5px 0; } .zone-bar { flex: 1; height: 8px; border-radius: 4px; position: relative; overflow: hidden; } .zone-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; } </style> </head> <body> <div class="cyber-container"> <header class="header"> <h1 class="neon-text">🌸 CyberRide Analyst 🚴♀️✨</h1> <p>Kawaii Cyberpunk Cycling Data Analysis</p> </header> <div class="upload-zone" id="uploadZone"> <div class="kawaii-emoji">🚴♀️💕</div> <h3>Upload Your Ride Data</h3> <p>Drop your CSV file here or click to browse</p> <input type="file" id="fileInput" class="file-input" accept=".csv" /> <button class="upload-btn" onclick="document.getElementById('fileInput').click()"> Choose File ✨ </button> </div> <div class="loading" id="loading"> <div class="kawaii-emoji">🔄</div> <p>Analyzing your cyber ride data... ✨</p> </div> <div id="results" class="hidden"> <div class="stats-grid" id="statsGrid"> <!-- Stats will be populated here --> </div> <div class="analysis-tabs"> <button class="tab-btn active" onclick="showTab('map')">🗺️ Map View</button> <button class="tab-btn" onclick="showTab('charts')">📊 Performance</button> <button class="tab-btn" onclick="showTab('heatmap')">🔥 Heat Zones</button> <button class="tab-btn" onclick="showTab('achievements')">🏆 Achievements</button> </div> <div id="mapTab" class="tab-content"> <div class="map-container" id="mapContainer"></div> </div> <div id="chartsTab" class="tab-content hidden"> <div class="chart-container" id="hrChart"></div> <div class="chart-container" id="speedChart"></div> <div class="chart-container" id="elevationChart"></div> </div> <div id="heatmapTab" class="tab-content hidden"> <div class="chart-container" id="heatmapChart"></div> <div class="chart-container" id="powerZones"></div> </div> <div id="achievementsTab" class="tab-content hidden"> <div class="chart-container" id="achievementsContainer"></div> </div> </div> </div> <script> let rideData = []; let map; let currentTab = 'map'; // File upload handling document.getElementById('fileInput').addEventListener('change', handleFileUpload); document.getElementById('uploadZone').addEventListener('click', () => { document.getElementById('fileInput').click(); }); // Drag and drop document.getElementById('uploadZone').addEventListener('dragover', (e) => { e.preventDefault(); e.currentTarget.style.borderColor = '#ff6b9d'; }); document.getElementById('uploadZone').addEventListener('dragleave', (e) => { e.preventDefault(); e.currentTarget.style.borderColor = '#4ecdc4'; }); document.getElementById('uploadZone').addEventListener('drop', (e) => { e.preventDefault(); e.currentTarget.style.borderColor = '#4ecdc4'; const files = e.dataTransfer.files; if (files.length > 0) { processFile(files[0]); } }); function handleFileUpload(event) { const file = event.target.files[0]; if (file) { processFile(file); } } function processFile(file) { document.getElementById('loading').classList.add('show'); const reader = new FileReader(); reader.onload = function(e) { const csv = e.target.result; parseCSV(csv); }; reader.readAsText(file); } function parseCSV(csv) { const lines = csv.split('\n'); const headers = lines[0].split(','); rideData = []; for (let i = 1; i < lines.length; i++) { if (lines[i].trim()) { const values = lines[i].split(','); const point = {}; headers.forEach((header, index) => { point[header.trim()] = values[index]?.trim() || ''; }); // Only include points with valid location data and movement if (point.latitude && point.longitude && point.distance !== '' && parseFloat(point.distance) > 0) { point.latitude = parseFloat(point.latitude); point.longitude = parseFloat(point.longitude); point.elevation = parseFloat(point.elevation) || 0; point.distance = parseFloat(point.distance) || 0; point.hr = parseInt(point.hr) || 0; point.since_start = parseInt(point.since_start) || 0; rideData.push(point); } } } if (rideData.length > 0) { calculateDerivedData(); displayResults(); } else { alert('😿 No valid ride data found! Please check your CSV format.'); } document.getElementById('loading').classList.remove('show'); } function calculateDerivedData() { for (let i = 0; i < rideData.length; i++) { const point = rideData[i]; // Calculate speed (km/h) if (i > 0) { const prevPoint = rideData[i - 1]; const timeDiff = (point.since_start - prevPoint.since_start) / 3600; // hours const distDiff = point.distance - prevPoint.distance; point.speed = timeDiff > 0 ? distDiff / timeDiff : 0; } else { point.speed = 0; } // Calculate METS (approximate for cycling) point.mets = point.speed ? Math.max(2, point.speed * 0.5) : 0; // Heart rate zones if (point.hr > 0) { if (point.hr < 100) point.hrZone = 1; else if (point.hr < 120) point.hrZone = 2; else if (point.hr < 140) point.hrZone = 3; else if (point.hr < 160) point.hrZone = 4; else point.hrZone = 5; } else { point.hrZone = 0; } } } function displayResults() { document.getElementById('results').classList.remove('hidden'); createStatsGrid(); createMap(); createCharts(); createHeatmap(); createAchievements(); } function createStatsGrid() { const validHRData = rideData.filter(p => p.hr > 0); const validSpeedData = rideData.filter(p => p.speed > 0); const stats = { totalDistance: Math.max(...rideData.map(p => p.distance)), maxSpeed: Math.max(...validSpeedData.map(p => p.speed)), avgSpeed: validSpeedData.reduce((a, b) => a + b.speed, 0) / validSpeedData.length, maxHR: Math.max(...validHRData.map(p => p.hr)), avgHR: validHRData.reduce((a, b) => a + b.hr, 0) / validHRData.length, maxElevation: Math.max(...rideData.map(p => p.elevation)), avgMETS: rideData.reduce((a, b) => a + b.mets, 0) / rideData.length, duration: Math.max(...rideData.map(p => p.since_start)) / 60 // minutes }; const statsHTML = ` <div class="stat-card"> <div class="stat-title">🚴♀️ Total Distance</div> <div class="stat-value">${stats.totalDistance.toFixed(2)} km</div> </div> <div class="stat-card"> <div class="stat-title">⚡ Max Speed</div> <div class="stat-value">${stats.maxSpeed.toFixed(1)} km/h</div> </div> <div class="stat-card"> <div class="stat-title">📊 Avg Speed</div> <div class="stat-value">${stats.avgSpeed.toFixed(1)} km/h</div> </div> <div class="stat-card"> <div class="stat-title">💖 Max Heart Rate</div> <div class="stat-value">${stats.maxHR} bpm</div> </div> <div class="stat-card"> <div class="stat-title">💗 Avg Heart Rate</div> <div class="stat-value">${stats.avgHR.toFixed(0)} bpm</div> </div> <div class="stat-card"> <div class="stat-title">⛰️ Max Elevation</div> <div class="stat-value">${stats.maxElevation.toFixed(0)} m</div> </div> <div class="stat-card"> <div class="stat-title">🔥 Avg METS</div> <div class="stat-value">${stats.avgMETS.toFixed(1)}</div> </div> <div class="stat-card"> <div class="stat-title">⏱️ Duration</div> <div class="stat-value">${Math.floor(stats.duration)}m</div> </div> `; document.getElementById('statsGrid').innerHTML = statsHTML; } function createMap() { if (map) { map.remove(); } map = L.map('mapContainer'); L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png').addTo(map); const bounds = L.latLngBounds(rideData.map(p => [p.latitude, p.longitude])); map.fitBounds(bounds, { padding: [10, 10] }); // Create colored segments based on heart rate zones const hrColors = { 0: '#666666', // No HR data 1: '#4ecdc4', // Zone 1 - Easy 2: '#45b7aa', // Zone 2 - Moderate 3: '#ffd700', // Zone 3 - Vigorous 4: '#ff8c42', // Zone 4 - Hard 5: '#ff6b9d' // Zone 5 - Maximum }; for (let i = 1; i < rideData.length; i++) { const start = rideData[i - 1]; const end = rideData[i]; const color = hrColors[end.hrZone] || '#666666'; L.polyline([[start.latitude, start.longitude], [end.latitude, end.longitude]], { color: color, weight: 4, opacity: 0.8 }).bindPopup(` <div style="color: black; font-family: Orbitron;"> <strong>💗 HR:</strong> ${end.hr} bpm<br> <strong>⚡ Speed:</strong> ${end.speed.toFixed(1)} km/h<br> <strong>🔥 METS:</strong> ${end.mets.toFixed(1)}<br> <strong>📍 Elevation:</strong> ${end.elevation.toFixed(0)}m </div> `).addTo(map); } // Start and end markers if (rideData.length > 0) { L.marker([rideData[0].latitude, rideData[0].longitude]) .addTo(map) .bindPopup('<div style="color: black;">🏁 Start Point</div>'); L.marker([rideData[rideData.length - 1].latitude, rideData[rideData.length - 1].longitude]) .addTo(map) .bindPopup('<div style="color: black;">🎌 Finish Point</div>'); } } function createCharts() { const times = rideData.map(p => p.since_start / 60); // minutes // Heart Rate Chart Plotly.newPlot('hrChart', [{ x: times, y: rideData.map(p => p.hr), type: 'scatter', mode: 'lines', line: { color: '#ff6b9d', width: 2 }, name: 'Heart Rate' }], { title: { text: '💗 Heart Rate Over Time', font: { color: '#4ecdc4', family: 'Orbitron' } }, xaxis: { title: 'Time (minutes)', color: '#4ecdc4' }, yaxis: { title: 'Heart Rate (bpm)', color: '#4ecdc4' }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', font: { color: '#4ecdc4', family: 'Orbitron' } }); // Speed Chart Plotly.newPlot('speedChart', [{ x: times, y: rideData.map(p => p.speed), type: 'scatter', mode: 'lines', line: { color: '#4ecdc4', width: 2 }, name: 'Speed' }], { title: { text: '⚡ Speed Over Time', font: { color: '#4ecdc4', family: 'Orbitron' } }, xaxis: { title: 'Time (minutes)', color: '#4ecdc4' }, yaxis: { title: 'Speed (km/h)', color: '#4ecdc4' }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', font: { color: '#4ecdc4', family: 'Orbitron' } }); // Elevation Chart Plotly.newPlot('elevationChart', [{ x: times, y: rideData.map(p => p.elevation), type: 'scatter', mode: 'lines', fill: 'tonexty', fillcolor: 'rgba(255, 215, 0, 0.3)', line: { color: '#ffd700', width: 2 }, name: 'Elevation' }], { title: { text: '⛰️ Elevation Profile', font: { color: '#4ecdc4', family: 'Orbitron' } }, xaxis: { title: 'Time (minutes)', color: '#4ecdc4' }, yaxis: { title: 'Elevation (m)', color: '#4ecdc4' }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', font: { color: '#4ecdc4', family: 'Orbitron' } }); } function createHeatmap() { // Heart Rate vs Speed Heatmap Plotly.newPlot('heatmapChart', [{ x: rideData.map(p => p.hr), y: rideData.map(p => p.speed), type: 'histogram2d', colorscale: [[0, '#0f0f23'], [0.5, '#4ecdc4'], [1, '#ff6b9d']], showscale: true }], { title: { text: '🔥 Heart Rate vs Speed Heatmap', font: { color: '#4ecdc4', family: 'Orbitron' } }, xaxis: { title: 'Heart Rate (bpm)', color: '#4ecdc4' }, yaxis: { title: 'Speed (km/h)', color: '#4ecdc4' }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', font: { color: '#4ecdc4', family: 'Orbitron' } }); // Power Zones const zoneNames = ['Rest', 'Easy', 'Moderate', 'Vigorous', 'Hard', 'Maximum']; const zoneColors = ['#666666', '#4ecdc4', '#45b7aa', '#ffd700', '#ff8c42', '#ff6b9d']; const zoneCounts = [0, 0, 0, 0, 0, 0]; rideData.forEach(p => zoneCounts[p.hrZone]++); let powerZonesHTML = '<h3 style="color: #4ecdc4; margin-bottom: 20px;">💪 Training Zones Distribution</h3>'; zoneCounts.forEach((count, index) => { const percentage = (count / rideData.length * 100).toFixed(1); powerZonesHTML += ` <div class="power-zone"> <span style="width: 100px; color: ${zoneColors[index]};">Zone ${index}:</span> <span style="width: 80px;">${zoneNames[index]}</span> <div class="zone-bar" style="background: rgba(255,255,255,0.1);"> <div class="zone-fill" style="width: ${percentage}%; background: ${zoneColors[index]};"></div> </div> <span style="width: 60px; text-align: right;">${percentage}%</span> </div> `; }); document.getElementById('powerZones').innerHTML = powerZonesHTML; } function createAchievements() { const validHRData = rideData.filter(p => p.hr > 0); const validSpeedData = rideData.filter(p => p.speed > 0); const achievements = []; // Speed achievements const maxSpeed = Math.max(...validSpeedData.map(p => p.speed)); if (maxSpeed > 50) achievements.push('🚀 Speed Demon'); if (maxSpeed > 40) achievements.push('⚡ Lightning Fast'); if (maxSpeed > 30) achievements.push('🌪️ Wind Walker'); // Distance achievements const totalDistance = Math.max(...rideData.map(p => p.distance)); if (totalDistance > 100) achievements.push('🏆 Century Rider'); if (totalDistance > 50) achievements.push('🌟 Long Distance'); if (totalDistance > 20) achievements.push('🚴♀️ Steady Rider'); // Heart rate achievements const maxHR = Math.max(...validHRData.map(p => p.hr)); if (maxHR > 180) achievements.push('💗 Heart Warrior'); if (maxHR > 160) achievements.push('💖 Cardio Master'); // Elevation achievements const maxElevation = Math.max(...rideData.map(p => p.elevation)); const minElevation = Math.min(...rideData.map(p => p.elevation)); const elevationGain = maxElevation - minElevation; if (elevationGain > 500) achievements.push('⛰️ Mountain Climber'); if (elevationGain > 200) achievements.push('🏔️ Hill Hunter'); // Consistency achievements const avgSpeed = validSpeedData.reduce((a, b) => a + b.speed, 0) / validSpeedData.length; const speedVariance = validSpeedData.reduce((a, b) => a + Math.pow(b.speed - avgSpeed, 2), 0) / validSpeedData.length; if (speedVariance < 10) achievements.push('🎯 Consistent Performer'); // Zone achievements const zone4and5Time = rideData.filter(p => p.hrZone >= 4).length; const totalTime = rideData.length; if (zone4and5Time / totalTime > 0.3) achievements.push('🔥 High Intensity Hero'); // Surprise Feature 1: AI Cycling Personality const personality = generateCyclingPersonality(); // Surprise Feature 2: Mood Ring Analysis const moodAnalysis = analyzeMoodFromData(); let achievementsHTML = ` <div style="margin-bottom: 30px;"> <h3 style="color: #4ecdc4; margin-bottom: 15px;">🏆 Achievements Unlocked</h3> ${achievements.map(achievement => `<span class="achievement-badge">${achievement}</span>`).join('')} </div> <div style="margin-bottom: 30px;"> <h3 style="color: #ff6b9d; margin-bottom: 15px;">🤖 AI Cycling Personality</h3> <div style="background: rgba(255, 107, 157, 0.1); padding: 20px; border-radius: 15px; border: 1px solid #ff6b9d;"> <h4 style="color: #ff6b9d; margin-bottom: 10px;">${personality.type}</h4> <p style="margin-bottom: 10px;">${personality.description}</p> <p><strong>Strengths:</strong> ${personality.strengths}</p> <p><strong>Recommendations:</strong> ${personality.recommendations}</p> </div> </div> <div> <h3 style="color: #4ecdc4; margin-bottom: 15px;">🌈 Ride Mood Ring Analysis</h3> <div style="background: rgba(78, 205, 196, 0.1); padding: 20px; border-radius: 15px; border: 1px solid #4ecdc4;"> <div style="display: flex; align-items: center; margin-bottom: 15px;"> <div style="width: 30px; height: 30px; border-radius: 50%; background: ${moodAnalysis.color}; margin-right: 15px;"></div> <h4 style="color: #4ecdc4;">${moodAnalysis.mood}</h4> </div> <p style="margin-bottom: 10px;">${moodAnalysis.analysis}</p> <p><strong>Energy Pattern:</strong> ${moodAnalysis.pattern}</p> </div> </div> `; document.getElementById('achievementsContainer').innerHTML = achievementsHTML; } function generateCyclingPersonality() { const validSpeedData = rideData.filter(p => p.speed > 0); const validHRData = rideData.filter(p => p.hr > 0); const avgSpeed = validSpeedData.reduce((a, b) => a + b.speed, 0) / validSpeedData.length; const maxSpeed = Math.max(...validSpeedData.map(p => p.speed)); const avgHR = validHRData.reduce((a, b) => a + b.hr, 0) / validHRData.length; const speedRange = maxSpeed - Math.min(...validSpeedData.map(p => p.speed)); const highIntensityTime = rideData.filter(p => p.hrZone >= 4).length / rideData.length; if (avgSpeed > 25 && highIntensityTime > 0.4) { return { type: "🚀 Cyber Speed Demon", description: "You're a high-octane rider who loves pushing limits! Your data shows aggressive acceleration patterns and sustained high-intensity efforts.", strengths: "Raw power, competitive spirit, threshold training", recommendations: "Try interval training and hill repeats to maximize your natural speed tendencies!" }; } else if (speedRange < 10 && highIntensityTime < 0.2) { return { type: "🧘♀️ Zen Cruise Master", description: "You're the epitome of smooth, consistent riding. Your heart rate and speed data show remarkable stability and control.", strengths: "Endurance, consistency, energy management", recommendations: "Perfect for long-distance rides! Consider adding some tempo work to expand your range." }; } else if (avgHR > 150 && highIntensityTime > 0.3) { return { type: "💗 Cardio Ninja", description: "Your heart is your engine! You love working in higher heart rate zones and aren't afraid of the burn.", strengths: "Cardiovascular fitness, recovery, lactate threshold", recommendations: "You'd excel in climbing challenges and endurance events!" }; } else { return { type: "🌈 Balanced Explorer", description: "You've found the sweet spot between intensity and endurance. Your riding style is adaptable and well-rounded.", strengths: "Versatility, adaptability, all-around fitness", recommendations: "Try mixing up terrains and adding variety to your routes for continued growth!" }; } } function analyzeMoodFromData() { const validHRData = rideData.filter(p => p.hr > 0); const validSpeedData = rideData.filter(p => p.speed > 0); if (validHRData.length === 0 || validSpeedData.length === 0) { return { mood: "Mysterious Rider", color: "#666666", analysis: "Your ride data is as enigmatic as the neon-lit streets of Neo-Tokyo!", pattern: "Stealth mode activated" }; } const avgHR = validHRData.reduce((a, b) => a + b.hr, 0) / validHRData.length; const hrVariability = Math.sqrt(validHRData.reduce((a, b) => a + Math.pow(b.hr - avgHR, 2), 0) / validHRData.length); const avgSpeed = validSpeedData.reduce((a, b) => a + b.speed, 0) / validSpeedData.length; const speedVariability = Math.sqrt(validSpeedData.reduce((a, b) => a + Math.pow(b.speed - avgSpeed, 2), 0) / validSpeedData.length); const highIntensityTime = rideData.filter(p => p.hrZone >= 4).length / rideData.length; if (highIntensityTime > 0.4 && hrVariability > 15) { return { mood: "Electric Storm ⚡", color: "#ff6b9d", analysis: "Your ride was an emotional rollercoaster! High intensity with lots of variation suggests you were feeling adventurous and ready to challenge yourself.", pattern: "Wild and energetic with lightning bursts" }; } else if (hrVariability < 8 && speedVariability < 5) { return { mood: "Peaceful Flow 🌊", color: "#4ecdc4", analysis: "You were in the zone! Low variability in both heart rate and speed indicates a calm, meditative state during your ride.", pattern: "Smooth and harmonious like digital waves" }; } else if (avgHR > 150) { return { mood: "Determined Fire 🔥", color: "#ffd700", analysis: "You came to work! High average heart rate suggests you were focused and determined to push your limits today.", pattern: "Consistent burn with occasional flare-ups" }; } else if (speedVariability > 8) { return { mood: "Playful Explorer 🌈", color: "#45b7aa", analysis: "You were having fun out there! Speed variations suggest you were playing with pace, exploring different rhythms throughout your ride.", pattern: "Playful undulations like digital music" }; } else { return { mood: "Steady Warrior 💪", color: "#ff8c42", analysis: "Rock solid performance! Your data shows the mindset of a steady, reliable rider who knows their capabilities.", pattern: "Strong and consistent like cybernetic precision" }; } } function showTab(tabName) { // Hide all tab contents document.querySelectorAll('.tab-content').forEach(tab => { tab.classList.add('hidden'); }); // Remove active class from all buttons document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.remove('active'); }); // Show selected tab document.getElementById(tabName + 'Tab').classList.remove('hidden'); // Add active class to clicked button event.target.classList.add('active'); currentTab = tabName; // Refresh map if switching to map tab if (tabName === 'map' && map) { setTimeout(() => { map.invalidateSize(); }, 100); } } </script> </body> </html> ``` This cyberpunk kawaii cycle ride analyzer includes all the requested features plus two exciting surprises: 🤖 **Surprise Feature 1: AI Cycling Personality Analysis** - Analyzes your riding patterns to determine your cycling personality type - Provides personalized recommendations based on your data patterns - Types include Speed Demon, Zen Cruise Master, Cardio Ninja, and Balanced Explorer 🌈 **Surprise Feature 2: Mood Ring Analysis** - Uses heart rate variability, speed patterns, and intensity zones to analyze your emotional state during the ride - Assigns a mood color and describes your energy pattern - Provides insights like "Electric Storm," "Peaceful Flow," "Determined Fire," etc. The app features: - **Cyberpunk aesthetic** with neon colors, glowing effects, and futuristic fonts - **Kawaii elements** with cute emojis and friendly language - **Interactive map** with heart rate zone coloring - **Comprehensive statistics** including speed, heart rate, elevation, and METS - **Multiple chart views** for performance analysis - **Heat zone visualization** showing training intensity distribution - **Achievement system** with unlockable badges - **Drag & drop file upload** with visual feedback The interface automatically filters out stationary data points and calculates derived metrics like speed, METS, and heart rate zones for comprehensive analysis.