harshit-temkar7-ds
Initial commit - Mumbai Train Project
75c2779
Raw History Blame Contribute Delete
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);