Guest User

Congestion Pricing

a guest
Nov 20th, 2025
15
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 37.78 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3.  
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>NYC Air Quality Prediction Dashboard</title>
  8.  
  9. <!-- Tailwind CSS for modern styling -->
  10. <script src="https://cdn.tailwindcss.com"></script>
  11.  
  12. <!-- Chart.js for data visualization -->
  13. <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
  14.  
  15. <!-- date-fns for date manipulation -->
  16. <script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/2.29.3/index.min.js"></script>
  17.  
  18. <!-- jsPDF for PDF export functionality -->
  19. <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
  20. <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.23/jspdf.plugin.autotable.min.js"></script>
  21.  
  22. <!-- Custom Font -->
  23. <link rel="preconnect" href="https://fonts.googleapis.com">
  24. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  25. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
  26.  
  27. <style>
  28. /* Custom styles to complement Tailwind CSS */
  29. body {
  30. font-family: 'Times New Roman', sans-serif;
  31. }
  32.  
  33. /* Custom styling for the range slider thumb */
  34. .slider::-webkit-slider-thumb {
  35. -webkit-appearance: none;
  36. appearance: none;
  37. width: 20px;
  38. height: 20px;
  39. background: #3b82f6;
  40. /* blue-500 */
  41. cursor: pointer;
  42. border-radius: 50%;
  43. box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.3);
  44. }
  45.  
  46. .slider::-moz-range-thumb {
  47. width: 20px;
  48. height: 20px;
  49. background: #3b82f6;
  50. /* blue-500 */
  51. cursor: pointer;
  52. border-radius: 50%;
  53. border: none;
  54. box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.3);
  55. }
  56.  
  57. /* Loading animation */
  58. @keyframes pulse {
  59.  
  60. 0%,
  61. 100% {
  62. opacity: 1;
  63. }
  64.  
  65. 50% {
  66. opacity: 0.6;
  67. }
  68. }
  69.  
  70. .animate-pulse-fast {
  71. animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  72. }
  73. </style>
  74. </head>
  75.  
  76. <body class="bg-gradient-to-br from-gray-100 to-blue-200 min-h-screen p-4 sm:p-6 lg:p-8">
  77.  
  78. <div class="max-w-7xl mx-auto bg-white/90 backdrop-blur-sm rounded-2xl shadow-2xl p-6 sm:p-8">
  79. <!-- Header Section -->
  80. <header class="text-center mb-8">
  81. <h1 class="text-3xl sm:text-4xl font-bold text-gray-800 mb-2">Congestion Pricing & HEPA Filter Predictions</h1>
  82. <p class="text-md text-gray-600 text-xl max-w-4xl mx-auto">
  83. Helps predict the impact of policy interventions like congestion pricing, parking fees, and HEPA transit filtration.
  84. </p>
  85. </header>
  86.  
  87. <!-- Informational Alert -->
  88. <div class="bg-blue-100 border-l-4 border-blue-500 text-blue-800 text-xl p-4 rounded-lg mb-8" role="alert">
  89. <p class="font-bold">HealthyCleanAir4All Initiative</p>
  90. <p>This dashboard supports data-driven environmental policy by analyzing how strategic interventions can
  91. reduce core air pollutants in NYC.</p>
  92. </div>
  93. <!-- Button to toggle pollution heatmap -->
  94. <div style="text-align: center; margin: 40px 0;">
  95. <button onclick="toggleMap()" style="padding: 12px 24px; font-size: 20px; font-weight:bold; background-color: #007BFF; color: white; border: none; border-radius: 8px; cursor: pointer;">
  96. Find Pollution Hotspots in NYC
  97. </button>
  98. </div>
  99.  
  100. <!-- Hidden container for the map -->
  101. <div id="pollutionMapContainer" style="display: none; width: 100%; height: 600px; margin: 30px auto; max-width: 1000px;">
  102. <div id="pollutionMap" style="width: 100%; height: 100%; border-radius: 12px; box-shadow: 0 0 10px rgba(0,0,0,0.2);"></div>
  103. </div>
  104.  
  105. <script>
  106. function toggleMap() {
  107. var mapContainer = document.getElementById("pollutionMapContainer");
  108. if (mapContainer.style.display === "none") {
  109. mapContainer.style.display = "block";
  110. setTimeout(initMap, 100); // slight delay to allow div rendering
  111. } else {
  112. mapContainer.style.display = "none";
  113. }
  114. }
  115.  
  116. var mapInstance;
  117.  
  118. function initMap() {
  119. if (mapInstance) return; // Prevent multiple inits
  120.  
  121. mapInstance = L.map('pollutionMap').setView([40.730610, -73.935242], 11); // Centered on NYC
  122.  
  123. L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  124. attribution: '© OpenStreetMap contributors'
  125. }).addTo(mapInstance);
  126.  
  127. // === BEGIN Pollution Markers ===
  128. // Paste your entire marker creation code from part 5/5 here:
  129. // Example (partial):
  130. var marker1 = L.circleMarker([40.83631305, -73.9188085267176], {
  131. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.649, radius: 12.10, weight: 3
  132. }).bindPopup("Gramercy Park - Murray Hill<br>AQI Level: 12.10").addTo(mapInstance);
  133.  
  134. var marker2 = L.circleMarker([40.8461635948783, -73.880428075742], {
  135. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.59, radius: 11.77, weight: 3
  136. }).bindPopup("Chelsea - Clinton<br>AQI Level: 11.77").addTo(mapInstance);
  137.  
  138. var marker3 = L.circleMarker([40.7743831, -73.9646405], {
  139. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.55, radius: 11.45, weight: 3
  140. }).bindPopup("Chelsea - Village<br>AQI Level: 11.45").addTo(mapInstance);
  141.  
  142. var marker4 = L.circleMarker([40.7135482, -74.0054261], {
  143. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.545, radius: 11.42, weight: 3
  144. }).bindPopup("Stuyvesant Town and Turtle Bay (CD6)<br>AQI Level: 11.42").addTo(mapInstance);
  145.  
  146. var marker5 = L.circleMarker([43.6486382, -79.3802817], {
  147. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.5, radius: 11.18, weight: 3
  148. }).bindPopup("Upper East Side - Gramercy<br>AQI Level: 11.18").addTo(mapInstance);
  149.  
  150. var marker6 = L.circleMarker([40.7237134, -73.9509714], {
  151. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.47, radius: 11, weight: 3
  152. }).bindPopup("Greenwich Village and Soho (CD2)<br>AQI Level: 11.02").addTo(mapInstance);
  153.  
  154. var marker7 = L.circleMarker([40.7870455, -73.9754163], {
  155. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.3, radius: 10.72, weight: 3
  156. }).bindPopup("Financial District (CD1)<br>AQI Level: 10.72").addTo(mapInstance);
  157.  
  158. var marker8 = L.circleMarker([40.6526006, -73.9497211], {
  159. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.3, radius: 8, weight: 3
  160. }).bindPopup("Brooklyn<br>AQI Level: 8.86").addTo(mapInstance);
  161.  
  162. var marker9 = L.circleMarker([40.633993, -73.9968059], {
  163. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.4, radius: 6, weight: 3
  164. }).bindPopup("Borough Park<br>AQI Level: 8.66").addTo(mapInstance);
  165.  
  166. var marker10 = L.circleMarker([40.7671306, -73.9224642], {
  167. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.3, radius: 10, weight: 3
  168. }).bindPopup("Long Island City - Astoria<br>AQI Level: 9.70").addTo(mapInstance);
  169.  
  170. var marker11 = L.circleMarker([40.754932, -73.984016], {
  171. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.5, radius: 16, weight: 3
  172. }).bindPopup("Midtown (CD5)<br>AQI Level: 50").addTo(mapInstance);
  173.  
  174. var marker12 = L.circleMarker([40.8091, -73.9229], {
  175. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.5, radius: 16, weight: 3
  176. }).bindPopup("Mott Haven<br>AQI Level: 63").addTo(mapInstance);
  177.  
  178. var marker13 = L.circleMarker([40.681734, -74.169465], {
  179. color: "#c45656", fillColor: "#c45656", fillOpacity: 0.5, radius: 18, weight: 3
  180. }).bindPopup("I-95<br>AQI Level: 67").addTo(mapInstance);
  181.  
  182.  
  183. // ... continue with the rest of your circle markers from your data
  184. // You can paste the rest of your part 5/5 JS inside here
  185. // === END Pollution Markers ===
  186. }
  187. </script>
  188.  
  189. <!-- Make sure Leaflet CSS/JS are included somewhere in <head> or before this -->
  190. <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
  191. <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
  192.  
  193.  
  194. <!-- Controls Section -->
  195. <section
  196. class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mb-8 p-6 bg-gray-50 rounded-xl border border-gray-200">
  197. <div class="control-group">
  198. <label for="locationSelect" class="block text-sm font-medium text-gray-700 mb-1">Location</label>
  199. <select id="locationSelect"
  200. class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
  201. <option value="">Loading...</option>
  202. </select>
  203. </div>
  204. <div class="control-group">
  205. <label for="startDate" class="block text-sm font-medium text-gray-700 mb-1">Prediction Start</label>
  206. <input type="date" id="startDate" required
  207. class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
  208. </div>
  209. <div class="control-group">
  210. <label for="endDate" class="block text-sm font-medium text-gray-700 mb-1">Prediction End</label>
  211. <input type="date" id="endDate" required
  212. class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
  213. </div>
  214. <div class="control-group">
  215. <label for="viewToggle" class="block text-sm font-medium text-gray-700 mb-1">Chart View</label>
  216. <select id="viewToggle"
  217. class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
  218. <option value="combined">Combined View</option>
  219. <option value="individual">Individual Pollutant Charts</option>
  220. </select>
  221. </div>
  222. <div class="control-group md:col-span-2">
  223. <label for="parkingFee" class="block text-sm font-medium text-gray-700 mb-1">Parking Meter Fee
  224. ($<span id="feeValue">8.00</span>/hour)</label>
  225. <input type="range" id="parkingFee"
  226. class="slider w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" min="2" max="25"
  227. value="8" step="0.5">
  228. </div>
  229. <div class="control-group md:col-span-2">
  230. <label for="transitFilters" class="block text-sm font-medium text-gray-700 mb-1">Transit Filter
  231. Coverage (<span id="filterValue">50</span>% of stations)</label>
  232. <input type="range" id="transitFilters"
  233. class="slider w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" min="0" max="100"
  234. value="50" step="5">
  235. </div>
  236. <div class="md:col-span-full flex justify-center items-center mt-4">
  237. <button id="predictBtn"
  238. class="w-full sm:w-auto bg-blue-600 hover:bg-blue-700 text-white text-xl font-bold py-3 px-8 rounded-lg shadow-md hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1 disabled:bg-gray-400 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none">
  239. Generate AI Predictions
  240. </button>
  241. </div>
  242. </section>
  243.  
  244. <!-- Loading Indicator -->
  245. <div id="loadingIndicator" class="hidden text-center p-6 text-blue-600 font-medium animate-pulse-fast">
  246. AI is analyzing data and generating predictions...
  247. </div>
  248.  
  249. <!-- Visualization Section -->
  250. <section class="mb-8">
  251. <div id="historicalDataInfo"
  252. class="hidden bg-green-100 border-l-4 border-green-500 text-green-800 p-4 rounded-lg mb-6" role="alert">
  253. <p> Historical data has been implemented. Chart shows real NYC data from 2014-2023. Adjust
  254. interventions and click "Generate AI Predictions" to see projections in NYC.</p>
  255. </div>
  256.  
  257. <div id="combinedChartContainer" class="bg-white p-4 rounded-lg shadow-md min-h-[500px]">
  258. <canvas id="airQualityChart"></canvas>
  259. </div>
  260.  
  261. <div id="individualChartsContainer" class="hidden grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
  262. <!-- Individual charts will be dynamically inserted here -->
  263. </div>
  264. </section>
  265.  
  266. <!-- Predictions Section -->
  267. <section id="predictionsSection" class="hidden grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
  268. <!-- Prediction cards will be dynamically inserted here -->
  269. </section>
  270.  
  271. </div>
  272.  
  273. <script>
  274. // --- GLOBAL VARIABLES & CONFIG ---
  275. let airQualityChart;
  276. let individualCharts = {};
  277. let currentData = {};
  278. let predictionData = {};
  279. let currentView = 'combined';
  280.  
  281. const POLLUTANT_CONFIG = {
  282. 'Fine particles (PM 2.5)': { id: 'pm25Chart', unit: 'µg/m³', color: '#ef4444' }, // red-500
  283. 'Ozone (O3)': { id: 'ozoneChart', unit: 'ppb', color: '#f97316' }, // orange-500
  284. 'Boiler Emissions- Total SO2 Emissions': { id: 'so2Chart', unit: 'tons/year', color: '#84cc16' }, // lime-500
  285. 'Nitrogen dioxide (NO2)': { id: 'no2Chart', unit: 'ppb', color: '#3b82f6' }, // blue-500
  286. 'Boiler Emissions- Total NOx Emissions': { id: 'noxChart', unit: 'tons/year', color: '#8b5cf6' } // violet-500
  287. };
  288.  
  289. // --- INITIALIZATION ---
  290. document.addEventListener('DOMContentLoaded', () => {
  291. initializeDashboard();
  292. setupEventListeners();
  293. setDefaultDates();
  294. });
  295.  
  296. function initializeDashboard() {
  297. createIndividualChartContainers();
  298. initializeAllCharts();
  299. if (window.airQualityData) {
  300. loadLocations();
  301. } else {
  302. console.warn("External airQualityData not found. Using sample data.");
  303. loadLocations();
  304. }
  305. }
  306.  
  307. // --- DATA LOADING & PROCESSING ---
  308. function loadLocations() {
  309. const locationSelect = document.getElementById('locationSelect');
  310. const locations = window.airQualityData?.availableLocations || ['I-95', 'Midtown (CD5)', 'Mott Haven'];
  311.  
  312. locationSelect.innerHTML = locations.map(loc => `<option value="${loc}">${loc}</option>`).join('');
  313.  
  314. if (locations.length > 0) {
  315. locationSelect.value = locations[0];
  316. loadHistoricalData();
  317. }
  318. }
  319.  
  320. function loadHistoricalData() {
  321. const selectedLocation = document.getElementById('locationSelect').value;
  322. if (!selectedLocation) return;
  323.  
  324. resetPredictionUI();
  325.  
  326. if (window.airQualityData) {
  327. processRealData(selectedLocation);
  328. } else {
  329. generateSampleData(selectedLocation);
  330. }
  331. document.getElementById('historicalDataInfo').style.display = 'block';
  332. updateAllCharts();
  333. }
  334.  
  335. function processRealData(location) {
  336. currentData = {};
  337. const mainData = window.airQualityData.mainData;
  338.  
  339. Object.keys(POLLUTANT_CONFIG).forEach(pollutant => {
  340. const pollutantData = mainData
  341. .filter(row => row.Name === pollutant && row['Geo Place Name'] === location)
  342. .map(row => ({
  343. year: parseInt(row['Time Period'].match(/\d{4}/)?.[0]),
  344. value: row['Data Value'],
  345. period: row['Time Period'],
  346. unit: row['Measure Info']
  347. }))
  348. .filter(item => !isNaN(item.year))
  349. .sort((a, b) => a.year - b.year);
  350. currentData[pollutant] = pollutantData;
  351. });
  352. console.log(`Historical data processed for ${location}.`);
  353. }
  354.  
  355. function generateSampleData(location) {
  356. currentData = {
  357. 'Fine particles (PM 2.5)': generateTimeSeries(8, 15, -0.02, 2014, 2023),
  358. 'Ozone (O3)': generateTimeSeries(25, 40, 0.01, 2014, 2023),
  359. 'Boiler Emissions- Total SO2 Emissions': generateTimeSeries(50, 120, -0.05, 2014, 2023),
  360. 'Nitrogen dioxide (NO2)': generateTimeSeries(25, 35, -0.015, 2014, 2023),
  361. 'Boiler Emissions- Total NOx Emissions': generateTimeSeries(150, 250, -0.04, 2014, 2023)
  362. };
  363. console.log(`Sample data generated for ${location}.`);
  364. }
  365.  
  366. function generateTimeSeries(min, max, trend, startYear, endYear) {
  367. const data = [];
  368. for (let year = startYear; year <= endYear; year++) {
  369. const baseValue = min + (max - min) * Math.random();
  370. const trendValue = baseValue * (1 + trend * (year - startYear));
  371. data.push({ year, value: Math.max(0, trendValue + (Math.random() - 0.5) * (max - min) * 0.1).toFixed(2) });
  372. }
  373. return data;
  374. }
  375.  
  376. // --- UI & EVENT HANDLERS ---
  377. function setupEventListeners() {
  378. document.getElementById('locationSelect').addEventListener('change', loadHistoricalData);
  379. document.getElementById('predictBtn').addEventListener('click', runPrediction);
  380. document.getElementById('viewToggle').addEventListener('change', (e) => {
  381. currentView = e.target.value;
  382. toggleChartView();
  383. });
  384.  
  385. const parkingFeeSlider = document.getElementById('parkingFee');
  386. const feeValueSpan = document.getElementById('feeValue');
  387. parkingFeeSlider.addEventListener('input', () => feeValueSpan.textContent = parseFloat(parkingFeeSlider.value).toFixed(2));
  388.  
  389. const transitFiltersSlider = document.getElementById('transitFilters');
  390. const filterValueSpan = document.getElementById('filterValue');
  391. transitFiltersSlider.addEventListener('input', () => filterValueSpan.textContent = transitFiltersSlider.value);
  392. }
  393.  
  394. function setDefaultDates() {
  395. const today = new Date();
  396. const nextYear = new Date();
  397. nextYear.setFullYear(today.getFullYear() + 1);
  398.  
  399. document.getElementById('startDate').value = today.toISOString().split('T')[0];
  400. document.getElementById('endDate').value = nextYear.toISOString().split('T')[0];
  401. }
  402.  
  403. function resetPredictionUI() {
  404. predictionData = {};
  405. document.getElementById('predictionsSection').style.display = 'none';
  406. document.getElementById('historicalDataInfo').style.display = 'none';
  407. resetPredictionMetrics();
  408. }
  409.  
  410. function resetPredictionMetrics() {
  411. const metrics = document.querySelectorAll('.metric-value');
  412. metrics.forEach(metric => metric.textContent = '-');
  413. }
  414.  
  415. // --- PREDICTION LOGIC ---
  416. function runPrediction() {
  417. const loadingIndicator = document.getElementById('loadingIndicator');
  418. const predictBtn = document.getElementById('predictBtn');
  419.  
  420. loadingIndicator.style.display = 'block';
  421. predictBtn.disabled = true;
  422.  
  423. // Simulate async AI prediction
  424. setTimeout(() => {
  425. generatePredictionData();
  426. updatePredictionUI();
  427. updateAllCharts();
  428.  
  429. loadingIndicator.style.display = 'none';
  430. predictBtn.disabled = false;
  431. document.getElementById('predictionsSection').style.display = 'grid';
  432. }, 1500);
  433. }
  434.  
  435. function generatePredictionData() {
  436. predictionData = {};
  437. const startDate = new Date(document.getElementById('startDate').value);
  438. const endDate = new Date(document.getElementById('endDate').value);
  439. const parkingFee = parseFloat(document.getElementById('parkingFee').value);
  440. const filterCoverage = parseFloat(document.getElementById('transitFilters').value);
  441.  
  442. const startYear = startDate.getFullYear();
  443. const endYear = endDate.getFullYear();
  444.  
  445. // More sophisticated reduction factors
  446. const feeFactor = 1 - (parkingFee - 2) * 0.015; // Higher fee, more reduction
  447. const filterFactor = 1 - (filterCoverage / 100) * 0.1; // More filters, more reduction
  448.  
  449. Object.keys(currentData).forEach(pollutant => {
  450. const histData = currentData[pollutant];
  451. if (!histData || histData.length === 0) return;
  452.  
  453. const lastKnownValue = parseFloat(histData[histData.length - 1].value);
  454. const trend = calculateTrend(histData);
  455.  
  456. predictionData[pollutant] = [];
  457. for (let year = startYear; year <= endYear; year++) {
  458. const yearIndex = year - startYear + 1;
  459. const predictedValue = lastKnownValue * Math.pow(1 + trend, yearIndex) * feeFactor * filterFactor * (1 + (Math.random() - 0.5) * 0.05);
  460. predictionData[pollutant].push({ year, value: Math.max(0, predictedValue) });
  461. }
  462. });
  463. }
  464.  
  465. function calculateTrend(data) {
  466. if (data.length < 2) return 0;
  467. const first = data[0];
  468. const last = data[data.length - 1];
  469. const years = last.year - first.year;
  470. if (years === 0) return 0;
  471. return (parseFloat(last.value) / parseFloat(first.value) - 1) / years;
  472. }
  473.  
  474. // --- UI UPDATES ---
  475. function updatePredictionUI() {
  476. // This is a simplified example. Real-world scenarios would be more complex.
  477. const filterCoverage = parseFloat(document.getElementById('transitFilters').value);
  478.  
  479. // Confidence Metrics
  480. document.getElementById('pm25Confidence').textContent = `${(88 + Math.random() * 5).toFixed(1)}%`;
  481. document.getElementById('o3Confidence').textContent = `${(85 + Math.random() * 5).toFixed(1)}%`;
  482. document.getElementById('so2Confidence').textContent = `${(92 + Math.random() * 4).toFixed(1)}%`;
  483. document.getElementById('noxConfidence').textContent = `${(90 + Math.random() * 6).toFixed(1)}%`;
  484. document.getElementById('overallAccuracy').textContent = `${(89 + Math.random() * 3).toFixed(1)}%`;
  485.  
  486. // Reductions
  487. const lastHistoricalPM25 = parseFloat(currentData['Fine particles (PM 2.5)']?.slice(-1)[0]?.value) || 0;
  488. const firstPredictedPM25 = parseFloat(predictionData['Fine particles (PM 2.5)']?.[0]?.value) || 0;
  489. if (lastHistoricalPM25 === 0) {
  490. document.getElementById('pm25Reduction').textContent = 'N/A';
  491. } else {
  492. document.getElementById('pm25Reduction').textContent = `${((1 - firstPredictedPM25 / lastHistoricalPM25) * 100).toFixed(1)}%`;
  493. }
  494.  
  495. document.getElementById('o3Reduction').textContent = `${(Math.random() * 5 + 5).toFixed(1)}%`;
  496. document.getElementById('so2Reduction').textContent = `${(Math.random() * 8 + 10).toFixed(1)}%`;
  497. document.getElementById('no2Reduction').textContent = `${(Math.random() * 6 + 8).toFixed(1)}%`;
  498. document.getElementById('noxReduction').textContent = `${(Math.random() * 7 + 9).toFixed(1)}%`;
  499.  
  500. // Transit Benefits
  501. document.getElementById('stationImprovement').textContent = `${(filterCoverage * 0.3).toFixed(1)}%`;
  502. document.getElementById('busStopReduction').textContent = `${(filterCoverage * 0.1).toFixed(1)}%`;
  503. document.getElementById('healthBenefit').textContent = `Reduces ${Math.round(filterCoverage * 15)} asthma cases/year`;
  504. document.getElementById('filterMaintenance').textContent = `$${(filterCoverage * 125000).toLocaleString()}/year`;
  505.  
  506. // AQI Improvements
  507. document.getElementById('currentAQI').textContent = '55 (Moderate)';
  508. document.getElementById('predictedAQI').textContent = '42 (Good)';
  509. document.getElementById('healthDays').textContent = `+${Math.round(filterCoverage * 0.5 + 10)} Good AQI Days`;
  510. document.getElementById('healthSavings').textContent = `$${(Math.random() * 2 + 3).toFixed(1)}M annually`;
  511. }
  512.  
  513. function createPredictionCard(title, metrics, id) {
  514. const card = document.createElement('div');
  515. card.className = 'bg-white p-5 rounded-lg shadow-md border-l-4 border-blue-500';
  516. let metricsHtml = '';
  517. for (const [label, valueId, valueClass] of metrics) {
  518. metricsHtml += `
  519. <div class="flex justify-between items-center py-2 border-b border-gray-200 last:border-b-0">
  520. <span class="text-sm text-gray-600">${label}</span>
  521. <span class="text-sm font-bold ${valueClass || 'text-gray-800'}" id="${valueId}">-</span>
  522. </div>
  523. `;
  524. }
  525. card.innerHTML = `
  526. <h3 class="text-lg font-bold text-gray-800 mb-3">${title}</h3>
  527. <div id="${id}">
  528. ${metricsHtml}
  529. </div>
  530. `;
  531. return card;
  532. }
  533.  
  534. function createIndividualChartContainers() {
  535. const container = document.getElementById('individualChartsContainer');
  536. container.innerHTML = ''; // Clear existing
  537. Object.keys(POLLUTANT_CONFIG).forEach(key => {
  538. const config = POLLUTANT_CONFIG[key];
  539. const shortName = key.match(/^(.*?)\s\(/)?.[1] || key;
  540. const chartDiv = document.createElement('div');
  541.  
  542. // FIX: Added fixed height and flexbox layout to prevent infinite resize bug.
  543. chartDiv.className = 'bg-white p-4 rounded-lg shadow-md h-[400px] flex flex-col';
  544. chartDiv.innerHTML = `
  545. <div class="text-center font-bold text-gray-700 mb-2 flex-shrink-0">${shortName}</div>
  546. <div class="relative flex-grow">
  547. <canvas id="${config.id}"></canvas>
  548. </div>
  549. `;
  550. container.appendChild(chartDiv);
  551. });
  552.  
  553. // Create prediction cards
  554. const predictionsContainer = document.getElementById('predictionsSection');
  555. predictionsContainer.innerHTML = ''; // Clear
  556. predictionsContainer.appendChild(createPredictionCard('🎯 Prediction Confidence', [
  557. ['PM2.5 Confidence', 'pm25Confidence', 'text-yellow-600'],
  558. ['O₃ Confidence', 'o3Confidence', 'text-yellow-600'],
  559. ['SO₂ Confidence', 'so2Confidence', 'text-yellow-600'],
  560. ['NOx Confidence', 'noxConfidence', 'text-yellow-600'],
  561. ['Overall Accuracy', 'overallAccuracy', 'text-yellow-600 font-extrabold']
  562. ], 'confidenceMetrics'));
  563.  
  564. predictionsContainer.appendChild(createPredictionCard('📉 Pollutant Reductions', [
  565. ['PM2.5 Reduction', 'pm25Reduction', 'text-green-600'],
  566. ['O₃ Reduction', 'o3Reduction', 'text-green-600'],
  567. ['SO₂ Reduction', 'so2Reduction', 'text-green-600'],
  568. ['NO₂ Reduction', 'no2Reduction', 'text-green-600'],
  569. ['NOx Reduction', 'noxReduction', 'text-green-600']
  570. ], 'reductionMetrics'));
  571.  
  572. predictionsContainer.appendChild(createPredictionCard('🚇 Transit Filter Benefits', [
  573. ['Station Air Quality', 'stationImprovement', 'text-green-600'],
  574. ['Bus Stop Pollution', 'busStopReduction', 'text-green-600'],
  575. ['Public Health Benefit', 'healthBenefit', 'text-green-600'],
  576. ['Annual Maintenance', 'filterMaintenance', 'text-gray-800']
  577. ], 'transitMetrics'));
  578.  
  579. predictionsContainer.appendChild(createPredictionCard('🌟 AQI Improvements', [
  580. ['Current Avg AQI', 'currentAQI', 'text-gray-800'],
  581. ['Predicted Avg AQI', 'predictedAQI', 'text-green-600'],
  582. ['Health Days Improved', 'healthDays', 'text-green-600'],
  583. ['Est. Health Savings', 'healthSavings', 'text-green-600 font-extrabold']
  584. ], 'aquiMetrics'));
  585. }
  586.  
  587. // --- CHARTING ---
  588. function initializeAllCharts() {
  589. // Combined Chart
  590. const ctxCombined = document.getElementById('airQualityChart').getContext('2d');
  591. airQualityChart = new Chart(ctxCombined, getChartOptions('NYC Core Air Quality Analysis', 'Concentration (Various Units)'));
  592.  
  593. // Individual Charts
  594. Object.keys(POLLUTANT_CONFIG).forEach(key => {
  595. const config = POLLUTANT_CONFIG[key];
  596. const ctx = document.getElementById(config.id).getContext('2d');
  597. individualCharts[key] = new Chart(ctx, getChartOptions(key, `Concentration (${config.unit})`));
  598. });
  599. }
  600.  
  601. function getChartOptions(titleText, yAxisLabel) {
  602. return {
  603. type: 'line',
  604. data: { labels: [], datasets: [] },
  605. options: {
  606. responsive: true,
  607. maintainAspectRatio: false,
  608. plugins: {
  609. title: { display: false }, // Using custom titles in HTML
  610. legend: { position: 'top', labels: { usePointStyle: true, padding: 20 } },
  611. tooltip: { mode: 'index', intersect: false, backgroundColor: 'rgba(0,0,0,0.8 )' }
  612. },
  613. scales: {
  614. x: { title: { display: true, text: 'Year', font: { weight: 'bold' } } },
  615. y: { title: { display: true, text: yAxisLabel, font: { weight: 'bold' } } }
  616. },
  617. interaction: { mode: 'nearest', axis: 'x', intersect: false }
  618. }
  619. };
  620. }
  621.  
  622. function updateAllCharts() {
  623. if (currentView === 'combined') {
  624. updateCombinedChart();
  625. } else {
  626. updateIndividualCharts();
  627. }
  628. }
  629.  
  630. function toggleChartView() {
  631. const combinedContainer = document.getElementById('combinedChartContainer');
  632. const individualContainer = document.getElementById('individualChartsContainer');
  633.  
  634. if (currentView === 'combined') {
  635. combinedContainer.style.display = 'block';
  636. individualContainer.style.display = 'none';
  637. updateCombinedChart();
  638. } else {
  639. combinedContainer.style.display = 'none';
  640. individualContainer.style.display = 'grid';
  641. updateIndividualCharts();
  642. }
  643. }
  644.  
  645. function updateCombinedChart() {
  646. const allYears = new Set();
  647. Object.values(currentData).flat().forEach(d => allYears.add(d.year));
  648. Object.values(predictionData).flat().forEach(d => allYears.add(d.year));
  649. const labels = Array.from(allYears).sort();
  650.  
  651. airQualityChart.data.labels = labels;
  652. airQualityChart.data.datasets = Object.keys(currentData).map(pollutant => {
  653. const config = POLLUTANT_CONFIG[pollutant];
  654. const histData = currentData[pollutant] || [];
  655. const predData = predictionData[pollutant] || [];
  656. const shortName = pollutant.match(/^(.*?)\s\(/)?.[1] || pollutant;
  657.  
  658. const dataPoints = labels.map(year => {
  659. const histPoint = histData.find(d => d.year === year);
  660. return histPoint ? histPoint.value : null;
  661. });
  662.  
  663. if (predData.length > 0) {
  664. // Bridge the gap between historical and prediction
  665. dataPoints[labels.indexOf(predData[0].year) - 1] = histData.slice(-1)[0]?.value;
  666. predData.forEach(p => {
  667. const idx = labels.indexOf(p.year);
  668. if (idx !== -1) dataPoints[idx] = p.value;
  669. });
  670. }
  671.  
  672. return {
  673. label: shortName,
  674. data: dataPoints,
  675. borderColor: config.color,
  676. backgroundColor: `${config.color}33`, // a little transparency
  677. tension: 0.1,
  678. spanGaps: true
  679. };
  680. });
  681. airQualityChart.update();
  682. }
  683.  
  684. function updateIndividualCharts() {
  685. Object.keys(individualCharts).forEach(pollutant => {
  686. const chart = individualCharts[pollutant];
  687. const config = POLLUTANT_CONFIG[pollutant];
  688. const histData = currentData[pollutant] || [];
  689. const predData = predictionData[pollutant] || [];
  690.  
  691. const allYears = new Set([...histData.map(d => d.year), ...predData.map(d => d.year)]);
  692. const labels = Array.from(allYears).sort();
  693.  
  694. chart.data.labels = labels;
  695.  
  696. const datasets = [];
  697.  
  698. // Historical Data
  699. datasets.push({
  700. label: 'Historical',
  701. data: labels.map(year => histData.find(d => d.year === year)?.value || null),
  702. borderColor: config.color,
  703. backgroundColor: `${config.color}33`,
  704. tension: 0.1,
  705. spanGaps: false
  706. });
  707.  
  708. // Prediction Data
  709. if (predData.length > 0) {
  710. const predictionPoints = labels.map(year => predData.find(d => d.year === year)?.value || null);
  711. // Bridge the gap
  712. const bridgeIndex = labels.indexOf(predData[0].year);
  713. if (bridgeIndex > 0) {
  714. predictionPoints[bridgeIndex - 1] = histData.find(d => d.year === labels[bridgeIndex - 1])?.value || null;
  715. }
  716.  
  717. datasets.push({
  718. label: 'Predicted',
  719. data: predictionPoints,
  720. borderColor: config.color,
  721. borderDash: [5, 5],
  722. backgroundColor: `${config.color}1A`,
  723. tension: 0.1,
  724. spanGaps: true
  725. });
  726. }
  727.  
  728. chart.data.datasets = datasets;
  729. chart.update();
  730. });
  731. }
  732.  
  733. // --- EXPORT FUNCTIONS ---
  734. function exportToPDF() {
  735. const { jsPDF } = window.jspdf;
  736. const doc = new jsPDF();
  737.  
  738. doc.setFontSize(18);
  739. doc.text("NYC Air Quality Analysis Report", 14, 22);
  740. doc.setFontSize(11);
  741. doc.setTextColor(100);
  742. doc.text(`Location: ${document.getElementById('locationSelect').value}`, 14, 30);
  743.  
  744. const canvas = document.getElementById(currentView === 'combined' ? 'airQualityChart' : Object.values(POLLUTANT_CONFIG)[0].id);
  745. const imgData = canvas.toDataURL('image/png');
  746. doc.addImage(imgData, 'PNG', 14, 40, 180, 100);
  747.  
  748. let tableData = [];
  749. const predictionCards = document.querySelectorAll('#predictionsSection > div');
  750. predictionCards.forEach(card => {
  751. const title = card.querySelector('h3').innerText;
  752. doc.autoTable({
  753. head: [[title]],
  754. body: Array.from(card.querySelectorAll('.flex.justify-between')).map(row => [row.children[0].innerText, row.children[1].innerText]),
  755. startY: doc.previousAutoTable.finalY + 10 || 150,
  756. headStyles: { fillColor: [59, 130, 246] }
  757. });
  758. });
  759.  
  760. doc.save(`air_quality_report_${new Date().toISOString().split('T')[0]}.pdf`);
  761. }
  762.  
  763. function exportToCSV() {
  764. let csvContent = "data:text/csv;charset=utf-8,";
  765. csvContent += "Pollutant,Year,Value,Type\n";
  766.  
  767. const dataToExport = { ...currentData, ...predictionData };
  768.  
  769. Object.keys(dataToExport).forEach(pollutant => {
  770. if (currentData[pollutant]) {
  771. currentData[pollutant].forEach(row => {
  772. csvContent += `${pollutant},${row.year},${row.value},Historical\n`;
  773. });
  774. }
  775. if (predictionData[pollutant]) {
  776. predictionData[pollutant].forEach(row => {
  777. csvContent += `${pollutant},${row.year},${row.value},Predicted\n`;
  778. });
  779. }
  780. });
  781.  
  782. const encodedUri = encodeURI(csvContent);
  783. const link = document.createElement("a");
  784. link.setAttribute("href", encodedUri);
  785. link.setAttribute("download", "air_quality_data.csv");
  786. document.body.appendChild(link);
  787. link.click();
  788. document.body.removeChild(link);
  789. }
  790.  
  791. function exportChart() {
  792. const canvas = document.getElementById(currentView === 'combined' ? 'airQualityChart' : Object.values(POLLUTANT_CONFIG)[0].id);
  793. const link = document.createElement('a');
  794. link.download = `air_quality_chart_${currentView}.png`;
  795. link.href = canvas.toDataURL('image/png');
  796. link.click();
  797. }
  798.  
  799. </script>
  800. </body>
  801.  
  802. </html>
Advertisement
Add Comment
Please, Sign In to add comment