Download static/js/app.js from HarryDataScience/mumbai-crowd-predictor: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/HarryDataScience/mumbai-crowd-predictor/resolve/main/static/js/app.js
- Command line
-
hf download hf://spaces/HarryDataScience/mumbai-crowd-predictor/static/js/app.js
-
curl -L -o app.js https://huggingface.co/spaces/HarryDataScience/mumbai-crowd-predictor/resolve/main/static/js/app.js
15.6 kB
| /* ββββββββββββββββββββββββββββββββββββββββββββββ | |
| Mumbai Local Train β Crowd Predictor JS | |
| ββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const API = ''; | |
| // ββ State ββββββββββββββββββββββββββββββββββββββ | |
| const state = { | |
| line: 'Western', | |
| station: '', | |
| destination: '', | |
| direction: 'Southbound', | |
| hour: 8, | |
| day: 'Monday', | |
| // forecast | |
| fcLine: 'Western', fcStation: '', fcDest: '', fcDir: 'Southbound', fcDay: 'Monday', | |
| // heatmap | |
| hmLine: 'Western', hmDir: 'Southbound', hmDay: 'Monday', | |
| }; | |
| const DAYS = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']; | |
| const HOUR_LABELS = { | |
| 5:'Early Morning',6:'Early Morning',7:'Morning Rush',8:'Peak Rush',9:'Morning Rush', | |
| 10:'Mid Morning',11:'Late Morning',12:'Noon',13:'Afternoon',14:'Afternoon', | |
| 15:'Mid Afternoon',16:'Late Afternoon',17:'Evening Rush',18:'Peak Evening', | |
| 19:'Evening Rush',20:'Late Evening',21:'Night',22:'Late Night' | |
| }; | |
| const COLOR_MAP = { | |
| Low: { color: '#10b981', bg: 'rgba(16,185,129,.15)', text: '#10b981' }, | |
| Medium: { color: '#f59e0b', bg: 'rgba(245,158,11,.15)', text: '#f59e0b' }, | |
| Extreme: { color: '#ef4444', bg: 'rgba(239,68,68,.15)', text: '#ef4444' }, | |
| }; | |
| // ββ DOM Helpers βββββββββββββββββββββββββββββββββ | |
| const $ = id => document.getElementById(id); | |
| const qs = sel => document.querySelector(sel); | |
| const qsa = sel => [...document.querySelectorAll(sel)]; | |
| function showToast(msg, dur = 3000) { | |
| let t = qs('.toast'); | |
| if (!t) { t = document.createElement('div'); t.className = 'toast'; document.body.append(t); } | |
| t.textContent = msg; | |
| t.classList.add('show'); | |
| setTimeout(() => t.classList.remove('show'), dur); | |
| } | |
| function showLoading(msg = 'Loadingβ¦') { | |
| let ov = qs('.loading-overlay'); | |
| if (!ov) { | |
| ov = document.createElement('div'); | |
| ov.className = 'loading-overlay'; | |
| ov.innerHTML = `<div class="loading-spinner"></div><div class="loading-text">${msg}</div>`; | |
| document.body.append(ov); | |
| } | |
| ov.querySelector('.loading-text').textContent = msg; | |
| } | |
| function hideLoading() { | |
| const ov = qs('.loading-overlay'); | |
| if (ov) ov.remove(); | |
| } | |
| async function apiFetch(url, options = {}) { | |
| const res = await fetch(API + url, options); | |
| if (!res.ok) throw new Error(await res.text()); | |
| return res.json(); | |
| } | |
| // ββ Init ββββββββββββββββββββββββββββββββββββββββ | |
| async function init() { | |
| showLoading('π Loading AI modelβ¦'); | |
| try { | |
| // Load stats | |
| const stats = await apiFetch('/api/stats'); | |
| if (stats.model_accuracy) { | |
| $('statAccuracy').textContent = stats.model_accuracy + '%'; | |
| $('statRecords').textContent = (stats.total_records / 1000).toFixed(0) + 'K+'; | |
| } | |
| // Populate days everywhere | |
| DAYS.forEach(d => { | |
| ['daySelect','fcDay','hmDay'].forEach(id => { | |
| const opt = document.createElement('option'); | |
| opt.value = opt.textContent = d; | |
| $(id)?.append(opt); | |
| }); | |
| }); | |
| // Load stations for default line | |
| await loadStations('Western', 'stationSelect', 'destSelect'); | |
| await loadStations('Western', 'fcStation', 'fcDest'); | |
| setupNavigation(); | |
| setupLinePills(); | |
| setupSlider(); | |
| setupPillGroups(); | |
| setupFormEvents(); | |
| $('predictBtn').addEventListener('click', handlePredict); | |
| $('fcBtn').addEventListener('click', handleForecast); | |
| $('hmBtn').addEventListener('click', handleHeatmap); | |
| hideLoading(); | |
| } catch (e) { | |
| hideLoading(); | |
| showToast('β οΈ Error connecting to server: ' + e.message, 5000); | |
| console.error(e); | |
| } | |
| } | |
| // ββ Navigation ββββββββββββββββββββββββββββββββββ | |
| function setupNavigation() { | |
| qsa('.nav-link').forEach(link => { | |
| link.addEventListener('click', e => { | |
| e.preventDefault(); | |
| const tab = link.dataset.tab; | |
| qsa('.nav-link').forEach(l => l.classList.remove('active')); | |
| link.classList.add('active'); | |
| qsa('.tab-section').forEach(s => { s.classList.remove('active'); s.classList.add('hidden'); }); | |
| const section = $('tab-' + tab); | |
| if (section) { section.classList.remove('hidden'); section.classList.add('active'); } | |
| }); | |
| }); | |
| } | |
| // ββ Stations ββββββββββββββββββββββββββββββββββββ | |
| async function loadStations(line, fromId, toId) { | |
| const data = await apiFetch('/api/stations/' + line); | |
| const stations = data.stations; | |
| [fromId, toId].forEach(id => { | |
| const sel = $(id); | |
| if (!sel) return; | |
| sel.innerHTML = '<option value="">Select Station</option>'; | |
| stations.forEach(s => { | |
| const opt = document.createElement('option'); | |
| opt.value = opt.textContent = s; | |
| sel.append(opt); | |
| }); | |
| }); | |
| // Auto-select defaults | |
| if ($(fromId) && stations.length > 0) $(fromId).value = stations[0]; | |
| if ($(toId) && stations.length > 1) $(toId).value = stations[stations.length - 1]; | |
| } | |
| // ββ Line Pills ββββββββββββββββββββββββββββββββββ | |
| function setupLinePills() { | |
| // Main predict | |
| qsa('#linePills .pill').forEach(pill => { | |
| pill.addEventListener('click', async () => { | |
| qsa('#linePills .pill').forEach(p => p.classList.remove('active')); | |
| pill.classList.add('active'); | |
| state.line = pill.dataset.val; | |
| await loadStations(state.line, 'stationSelect', 'destSelect'); | |
| }); | |
| }); | |
| // Forecast | |
| qsa('#fcLinePills .pill').forEach(pill => { | |
| pill.addEventListener('click', async () => { | |
| qsa('#fcLinePills .pill').forEach(p => p.classList.remove('active')); | |
| pill.classList.add('active'); | |
| state.fcLine = pill.dataset.val; | |
| await loadStations(state.fcLine, 'fcStation', 'fcDest'); | |
| }); | |
| }); | |
| // Heatmap | |
| qsa('#hmLinePills .pill').forEach(pill => { | |
| pill.addEventListener('click', () => { | |
| qsa('#hmLinePills .pill').forEach(p => p.classList.remove('active')); | |
| pill.classList.add('active'); | |
| state.hmLine = pill.dataset.val; | |
| }); | |
| }); | |
| } | |
| // ββ Pill Groups βββββββββββββββββββββββββββββββββ | |
| function setupPillGroups() { | |
| function bindPills(groupId, stateKey) { | |
| qsa(`#${groupId} .pill`).forEach(pill => { | |
| pill.addEventListener('click', () => { | |
| qsa(`#${groupId} .pill`).forEach(p => p.classList.remove('active')); | |
| pill.classList.add('active'); | |
| state[stateKey] = pill.dataset.val; | |
| }); | |
| }); | |
| } | |
| bindPills('dirPills', 'direction'); | |
| bindPills('fcDirPills', 'fcDir'); | |
| bindPills('hmDirPills', 'hmDir'); | |
| } | |
| // ββ Slider ββββββββββββββββββββββββββββββββββββββ | |
| function setupSlider() { | |
| const slider = $('hourSlider'); | |
| function updateHourDisplay(val) { | |
| const h = parseInt(val); | |
| state.hour = h; | |
| const label = HOUR_LABELS[h] || ''; | |
| $('hourDisplay').textContent = `${String(h).padStart(2,'0')}:00 (${label})`; | |
| } | |
| slider.addEventListener('input', e => updateHourDisplay(e.target.value)); | |
| updateHourDisplay(slider.value); | |
| } | |
| // ββ Form Events βββββββββββββββββββββββββββββββββ | |
| function setupFormEvents() { | |
| $('stationSelect').addEventListener('change', e => state.station = e.target.value); | |
| $('destSelect').addEventListener('change', e => state.destination = e.target.value); | |
| $('daySelect').addEventListener('change', e => state.day = e.target.value); | |
| $('fcStation').addEventListener('change', e => state.fcStation = e.target.value); | |
| $('fcDest').addEventListener('change', e => state.fcDest = e.target.value); | |
| $('fcDay').addEventListener('change', e => state.fcDay = e.target.value); | |
| $('hmDay').addEventListener('change', e => state.hmDay = e.target.value); | |
| } | |
| // ββ Predict βββββββββββββββββββββββββββββββββββββ | |
| async function handlePredict() { | |
| const station = $('stationSelect').value; | |
| const dest = $('destSelect').value; | |
| const day = $('daySelect').value; | |
| if (!station || !dest) { showToast('β οΈ Please select both stations'); return; } | |
| if (station === dest) { showToast('β οΈ Source and destination cannot be the same'); return; } | |
| const btn = $('predictBtn'); | |
| btn.classList.add('loading'); | |
| btn.querySelector('.btn-text').textContent = 'Predictingβ¦'; | |
| try { | |
| const payload = { | |
| line: state.line, station, destination: dest, | |
| direction: state.direction, hour: state.hour, day_of_week: day | |
| }; | |
| const result = await apiFetch('/api/predict', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| renderResult(result); | |
| } catch (e) { | |
| showToast('β Prediction failed: ' + e.message); | |
| } finally { | |
| btn.classList.remove('loading'); | |
| btn.querySelector('.btn-text').textContent = 'Predict Crowd Density'; | |
| } | |
| } | |
| function renderResult(r) { | |
| $('resultPlaceholder').classList.add('hidden'); | |
| $('resultContent').classList.remove('hidden'); | |
| const cm = COLOR_MAP[r.crowd_level] || COLOR_MAP.Low; | |
| // Route | |
| $('routeDisplay').innerHTML = `<span>${r.input.station}</span> β <span>${r.input.destination}</span><br> | |
| <small style="color:var(--text2)">${r.input.line} Line Β· ${r.input.direction} Β· ${String(r.input.hour).padStart(2,'0')}:00 Β· ${r.input.day}</small>`; | |
| // Badge | |
| const badge = $('riskBadge'); | |
| badge.textContent = r.risk_emoji + ' ' + r.risk_badge; | |
| badge.style.background = cm.bg; | |
| badge.style.color = cm.color; | |
| badge.style.border = `1px solid ${cm.color}40`; | |
| // Meter (score 0-100) | |
| const pct = r.risk_score; | |
| $('meterFill').style.width = (100 - pct) + '%'; | |
| $('meterPointer').style.left = pct + '%'; | |
| $('meterScore').textContent = pct + ' / 100'; | |
| $('meterScore').style.color = cm.color; | |
| // Prob bars | |
| const probHTML = Object.entries(r.probabilities).map(([level, pct]) => { | |
| const c = COLOR_MAP[level]?.color || '#94a3b8'; | |
| return `<div class="prob-row"> | |
| <span class="prob-label" style="color:${c}">${level}</span> | |
| <div class="prob-track"><div class="prob-fill" style="width:${pct}%;background:${c}"></div></div> | |
| <span class="prob-val">${pct}%</span> | |
| </div>`; | |
| }).join(''); | |
| $('probBars').innerHTML = probHTML; | |
| // Reasons | |
| const reasonsHTML = r.reasons.map(re => `<div class="reason-item">${re}</div>`).join(''); | |
| $('reasonsBox').innerHTML = `<div class="reasons-title">π WHY THIS PREDICTION</div>${reasonsHTML}`; | |
| // Tip | |
| $('tipBox').innerHTML = `<span>π‘</span><span>${r.travel_tip}</span>`; | |
| $('tipBox').style.color = cm.color; | |
| $('tipBox').style.borderColor = cm.color + '30'; | |
| $('tipBox').style.background = cm.bg; | |
| } | |
| // ββ Forecast βββββββββββββββββββββββββββββββββββββ | |
| async function handleForecast() { | |
| const station = $('fcStation').value; | |
| const dest = $('fcDest').value; | |
| const day = $('fcDay').value; | |
| if (!station || !dest) { showToast('β οΈ Please select stations'); return; } | |
| $('fcBtn').textContent = 'Loadingβ¦'; | |
| try { | |
| const result = await apiFetch('/api/hourly-forecast', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| line: state.fcLine, station, destination: dest, | |
| direction: state.fcDir, day_of_week: day | |
| }) | |
| }); | |
| renderForecast(result); | |
| } catch (e) { | |
| showToast('β Forecast failed: ' + e.message); | |
| } finally { | |
| $('fcBtn').textContent = 'View Forecast β'; | |
| } | |
| } | |
| function renderForecast(data) { | |
| const { hourly, best_hours, peak_hour } = data; | |
| const fr = $('forecastResults'); | |
| fr.classList.remove('hidden'); | |
| // Best times | |
| const bestHTML = ` | |
| <div class="best-title">β Best Times to Travel</div> | |
| <div class="best-grid"> | |
| ${best_hours.map((b, i) => { | |
| const cm = COLOR_MAP[b.crowd_level] || COLOR_MAP.Low; | |
| return `<div class="best-item" style="background:${cm.bg};border-color:${cm.color}40"> | |
| <div class="best-hour" style="color:${cm.color}">${b.label}</div> | |
| <div class="best-level" style="color:${cm.color}">${b.emoji} ${b.crowd_level}</div> | |
| <div class="best-label">Option ${i+1}</div> | |
| </div>`; | |
| }).join('')} | |
| <div class="best-item" style="background:rgba(239,68,68,.1);border-color:#ef444440"> | |
| <div class="best-hour" style="color:#ef4444">${peak_hour.label}</div> | |
| <div class="best-level" style="color:#ef4444">π΄ Peak Hour</div> | |
| <div class="best-label">Avoid</div> | |
| </div> | |
| </div>`; | |
| $('bestTimes').innerHTML = bestHTML; | |
| // Bar chart | |
| const maxScore = 100; | |
| const chartHTML = hourly.map(h => { | |
| const heightPct = (h.score / maxScore) * 140; | |
| return `<div class="chart-bar-wrap"> | |
| <div class="chart-bar" style="height:${heightPct}px;background:${h.color};opacity:0.85" | |
| data-tip="${h.label} β ${h.crowd_level} (${h.score})"></div> | |
| <div class="chart-tick">${h.hour}</div> | |
| </div>`; | |
| }).join(''); | |
| $('chartWrap').innerHTML = chartHTML; | |
| // Hourly grid | |
| const gridHTML = hourly.map(h => { | |
| const cm = COLOR_MAP[h.crowd_level] || COLOR_MAP.Low; | |
| return `<div class="hour-chip" style="background:${cm.bg};border-color:${cm.color}30"> | |
| <div class="hc-time" style="color:${cm.color}">${h.label}</div> | |
| <div class="hc-level" style="color:${cm.color}">${h.emoji} ${h.crowd_level}</div> | |
| <div class="hc-score">Risk: ${h.score}</div> | |
| </div>`; | |
| }).join(''); | |
| $('hourlyGrid').innerHTML = gridHTML; | |
| } | |
| // ββ Heatmap ββββββββββββββββββββββββββββββββββββββ | |
| async function handleHeatmap() { | |
| const day = $('hmDay').value; | |
| $('hmBtn').textContent = 'Loadingβ¦'; | |
| try { | |
| const result = await apiFetch(`/api/heatmap/${state.hmLine}?direction=${state.hmDir}&day=${day}`); | |
| renderHeatmap(result); | |
| } catch (e) { | |
| showToast('β Heatmap failed: ' + e.message); | |
| } finally { | |
| $('hmBtn').textContent = 'Load Heatmap β'; | |
| } | |
| } | |
| function renderHeatmap(data) { | |
| const { heatmap, hours, stations } = data; | |
| const wrap = $('heatmapWrap'); | |
| wrap.classList.remove('hidden'); | |
| const LEVEL_SHORT = { Low: 'L', Medium: 'M', Extreme: 'X' }; | |
| let html = '<table class="hm-table"><thead><tr><th>Station</th>'; | |
| hours.forEach(h => { html += `<th>${h}</th>`; }); | |
| html += '</tr></thead><tbody>'; | |
| heatmap.forEach(row => { | |
| html += `<tr><td class="hm-station">${row.station}</td>`; | |
| hours.forEach(h => { | |
| const cell = row.hours[h]; | |
| if (cell) { | |
| html += `<td style="background:${cell.color}25;color:${cell.color}" title="${row.station} at ${h}:00 β ${cell.level}">${LEVEL_SHORT[cell.level] || '?'}</td>`; | |
| } else { | |
| html += `<td>-</td>`; | |
| } | |
| }); | |
| html += '</tr>'; | |
| }); | |
| html += '</tbody></table>'; | |
| $('heatmapTable').innerHTML = html; | |
| } | |
| // ββ Boot βββββββββββββββββββββββββββββββββββββββββ | |
| document.addEventListener('DOMContentLoaded', init); | |