Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>NYC Air Quality Prediction Dashboard</title>
- <!-- Tailwind CSS for modern styling -->
- <script src="https://cdn.tailwindcss.com"></script>
- <!-- Chart.js for data visualization -->
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
- <!-- date-fns for date manipulation -->
- <script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/2.29.3/index.min.js"></script>
- <!-- jsPDF for PDF export functionality -->
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.23/jspdf.plugin.autotable.min.js"></script>
- <!-- Custom Font -->
- <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=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
- <style>
- /* Custom styles to complement Tailwind CSS */
- body {
- font-family: 'Times New Roman', sans-serif;
- }
- /* Custom styling for the range slider thumb */
- .slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 20px;
- height: 20px;
- background: #3b82f6;
- /* blue-500 */
- cursor: pointer;
- border-radius: 50%;
- box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.3);
- }
- .slider::-moz-range-thumb {
- width: 20px;
- height: 20px;
- background: #3b82f6;
- /* blue-500 */
- cursor: pointer;
- border-radius: 50%;
- border: none;
- box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.3);
- }
- /* Loading animation */
- @keyframes pulse {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.6;
- }
- }
- .animate-pulse-fast {
- animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- </style>
- </head>
- <body class="bg-gradient-to-br from-gray-100 to-blue-200 min-h-screen p-4 sm:p-6 lg:p-8">
- <div class="max-w-7xl mx-auto bg-white/90 backdrop-blur-sm rounded-2xl shadow-2xl p-6 sm:p-8">
- <!-- Header Section -->
- <header class="text-center mb-8">
- <h1 class="text-3xl sm:text-4xl font-bold text-gray-800 mb-2">Congestion Pricing & HEPA Filter Predictions</h1>
- <p class="text-md text-gray-600 text-xl max-w-4xl mx-auto">
- Helps predict the impact of policy interventions like congestion pricing, parking fees, and HEPA transit filtration.
- </p>
- </header>
- <!-- Informational Alert -->
- <div class="bg-blue-100 border-l-4 border-blue-500 text-blue-800 text-xl p-4 rounded-lg mb-8" role="alert">
- <p class="font-bold">HealthyCleanAir4All Initiative</p>
- <p>This dashboard supports data-driven environmental policy by analyzing how strategic interventions can
- reduce core air pollutants in NYC.</p>
- </div>
- <!-- Button to toggle pollution heatmap -->
- <div style="text-align: center; margin: 40px 0;">
- <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;">
- Find Pollution Hotspots in NYC
- </button>
- </div>
- <!-- Hidden container for the map -->
- <div id="pollutionMapContainer" style="display: none; width: 100%; height: 600px; margin: 30px auto; max-width: 1000px;">
- <div id="pollutionMap" style="width: 100%; height: 100%; border-radius: 12px; box-shadow: 0 0 10px rgba(0,0,0,0.2);"></div>
- </div>
- <script>
- function toggleMap() {
- var mapContainer = document.getElementById("pollutionMapContainer");
- if (mapContainer.style.display === "none") {
- mapContainer.style.display = "block";
- setTimeout(initMap, 100); // slight delay to allow div rendering
- } else {
- mapContainer.style.display = "none";
- }
- }
- var mapInstance;
- function initMap() {
- if (mapInstance) return; // Prevent multiple inits
- mapInstance = L.map('pollutionMap').setView([40.730610, -73.935242], 11); // Centered on NYC
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- attribution: '© OpenStreetMap contributors'
- }).addTo(mapInstance);
- // === BEGIN Pollution Markers ===
- // Paste your entire marker creation code from part 5/5 here:
- // Example (partial):
- var marker1 = L.circleMarker([40.83631305, -73.9188085267176], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.649, radius: 12.10, weight: 3
- }).bindPopup("Gramercy Park - Murray Hill<br>AQI Level: 12.10").addTo(mapInstance);
- var marker2 = L.circleMarker([40.8461635948783, -73.880428075742], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.59, radius: 11.77, weight: 3
- }).bindPopup("Chelsea - Clinton<br>AQI Level: 11.77").addTo(mapInstance);
- var marker3 = L.circleMarker([40.7743831, -73.9646405], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.55, radius: 11.45, weight: 3
- }).bindPopup("Chelsea - Village<br>AQI Level: 11.45").addTo(mapInstance);
- var marker4 = L.circleMarker([40.7135482, -74.0054261], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.545, radius: 11.42, weight: 3
- }).bindPopup("Stuyvesant Town and Turtle Bay (CD6)<br>AQI Level: 11.42").addTo(mapInstance);
- var marker5 = L.circleMarker([43.6486382, -79.3802817], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.5, radius: 11.18, weight: 3
- }).bindPopup("Upper East Side - Gramercy<br>AQI Level: 11.18").addTo(mapInstance);
- var marker6 = L.circleMarker([40.7237134, -73.9509714], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.47, radius: 11, weight: 3
- }).bindPopup("Greenwich Village and Soho (CD2)<br>AQI Level: 11.02").addTo(mapInstance);
- var marker7 = L.circleMarker([40.7870455, -73.9754163], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.3, radius: 10.72, weight: 3
- }).bindPopup("Financial District (CD1)<br>AQI Level: 10.72").addTo(mapInstance);
- var marker8 = L.circleMarker([40.6526006, -73.9497211], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.3, radius: 8, weight: 3
- }).bindPopup("Brooklyn<br>AQI Level: 8.86").addTo(mapInstance);
- var marker9 = L.circleMarker([40.633993, -73.9968059], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.4, radius: 6, weight: 3
- }).bindPopup("Borough Park<br>AQI Level: 8.66").addTo(mapInstance);
- var marker10 = L.circleMarker([40.7671306, -73.9224642], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.3, radius: 10, weight: 3
- }).bindPopup("Long Island City - Astoria<br>AQI Level: 9.70").addTo(mapInstance);
- var marker11 = L.circleMarker([40.754932, -73.984016], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.5, radius: 16, weight: 3
- }).bindPopup("Midtown (CD5)<br>AQI Level: 50").addTo(mapInstance);
- var marker12 = L.circleMarker([40.8091, -73.9229], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.5, radius: 16, weight: 3
- }).bindPopup("Mott Haven<br>AQI Level: 63").addTo(mapInstance);
- var marker13 = L.circleMarker([40.681734, -74.169465], {
- color: "#c45656", fillColor: "#c45656", fillOpacity: 0.5, radius: 18, weight: 3
- }).bindPopup("I-95<br>AQI Level: 67").addTo(mapInstance);
- // ... continue with the rest of your circle markers from your data
- // You can paste the rest of your part 5/5 JS inside here
- // === END Pollution Markers ===
- }
- </script>
- <!-- Make sure Leaflet CSS/JS are included somewhere in <head> or before this -->
- <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
- <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
- <!-- Controls Section -->
- <section
- 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">
- <div class="control-group">
- <label for="locationSelect" class="block text-sm font-medium text-gray-700 mb-1">Location</label>
- <select id="locationSelect"
- class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
- <option value="">Loading...</option>
- </select>
- </div>
- <div class="control-group">
- <label for="startDate" class="block text-sm font-medium text-gray-700 mb-1">Prediction Start</label>
- <input type="date" id="startDate" required
- class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
- </div>
- <div class="control-group">
- <label for="endDate" class="block text-sm font-medium text-gray-700 mb-1">Prediction End</label>
- <input type="date" id="endDate" required
- class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
- </div>
- <div class="control-group">
- <label for="viewToggle" class="block text-sm font-medium text-gray-700 mb-1">Chart View</label>
- <select id="viewToggle"
- class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
- <option value="combined">Combined View</option>
- <option value="individual">Individual Pollutant Charts</option>
- </select>
- </div>
- <div class="control-group md:col-span-2">
- <label for="parkingFee" class="block text-sm font-medium text-gray-700 mb-1">Parking Meter Fee
- ($<span id="feeValue">8.00</span>/hour)</label>
- <input type="range" id="parkingFee"
- class="slider w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" min="2" max="25"
- value="8" step="0.5">
- </div>
- <div class="control-group md:col-span-2">
- <label for="transitFilters" class="block text-sm font-medium text-gray-700 mb-1">Transit Filter
- Coverage (<span id="filterValue">50</span>% of stations)</label>
- <input type="range" id="transitFilters"
- class="slider w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" min="0" max="100"
- value="50" step="5">
- </div>
- <div class="md:col-span-full flex justify-center items-center mt-4">
- <button id="predictBtn"
- 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">
- Generate AI Predictions
- </button>
- </div>
- </section>
- <!-- Loading Indicator -->
- <div id="loadingIndicator" class="hidden text-center p-6 text-blue-600 font-medium animate-pulse-fast">
- AI is analyzing data and generating predictions...
- </div>
- <!-- Visualization Section -->
- <section class="mb-8">
- <div id="historicalDataInfo"
- class="hidden bg-green-100 border-l-4 border-green-500 text-green-800 p-4 rounded-lg mb-6" role="alert">
- <p> Historical data has been implemented. Chart shows real NYC data from 2014-2023. Adjust
- interventions and click "Generate AI Predictions" to see projections in NYC.</p>
- </div>
- <div id="combinedChartContainer" class="bg-white p-4 rounded-lg shadow-md min-h-[500px]">
- <canvas id="airQualityChart"></canvas>
- </div>
- <div id="individualChartsContainer" class="hidden grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
- <!-- Individual charts will be dynamically inserted here -->
- </div>
- </section>
- <!-- Predictions Section -->
- <section id="predictionsSection" class="hidden grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
- <!-- Prediction cards will be dynamically inserted here -->
- </section>
- </div>
- <script>
- // --- GLOBAL VARIABLES & CONFIG ---
- let airQualityChart;
- let individualCharts = {};
- let currentData = {};
- let predictionData = {};
- let currentView = 'combined';
- const POLLUTANT_CONFIG = {
- 'Fine particles (PM 2.5)': { id: 'pm25Chart', unit: 'µg/m³', color: '#ef4444' }, // red-500
- 'Ozone (O3)': { id: 'ozoneChart', unit: 'ppb', color: '#f97316' }, // orange-500
- 'Boiler Emissions- Total SO2 Emissions': { id: 'so2Chart', unit: 'tons/year', color: '#84cc16' }, // lime-500
- 'Nitrogen dioxide (NO2)': { id: 'no2Chart', unit: 'ppb', color: '#3b82f6' }, // blue-500
- 'Boiler Emissions- Total NOx Emissions': { id: 'noxChart', unit: 'tons/year', color: '#8b5cf6' } // violet-500
- };
- // --- INITIALIZATION ---
- document.addEventListener('DOMContentLoaded', () => {
- initializeDashboard();
- setupEventListeners();
- setDefaultDates();
- });
- function initializeDashboard() {
- createIndividualChartContainers();
- initializeAllCharts();
- if (window.airQualityData) {
- loadLocations();
- } else {
- console.warn("External airQualityData not found. Using sample data.");
- loadLocations();
- }
- }
- // --- DATA LOADING & PROCESSING ---
- function loadLocations() {
- const locationSelect = document.getElementById('locationSelect');
- const locations = window.airQualityData?.availableLocations || ['I-95', 'Midtown (CD5)', 'Mott Haven'];
- locationSelect.innerHTML = locations.map(loc => `<option value="${loc}">${loc}</option>`).join('');
- if (locations.length > 0) {
- locationSelect.value = locations[0];
- loadHistoricalData();
- }
- }
- function loadHistoricalData() {
- const selectedLocation = document.getElementById('locationSelect').value;
- if (!selectedLocation) return;
- resetPredictionUI();
- if (window.airQualityData) {
- processRealData(selectedLocation);
- } else {
- generateSampleData(selectedLocation);
- }
- document.getElementById('historicalDataInfo').style.display = 'block';
- updateAllCharts();
- }
- function processRealData(location) {
- currentData = {};
- const mainData = window.airQualityData.mainData;
- Object.keys(POLLUTANT_CONFIG).forEach(pollutant => {
- const pollutantData = mainData
- .filter(row => row.Name === pollutant && row['Geo Place Name'] === location)
- .map(row => ({
- year: parseInt(row['Time Period'].match(/\d{4}/)?.[0]),
- value: row['Data Value'],
- period: row['Time Period'],
- unit: row['Measure Info']
- }))
- .filter(item => !isNaN(item.year))
- .sort((a, b) => a.year - b.year);
- currentData[pollutant] = pollutantData;
- });
- console.log(`Historical data processed for ${location}.`);
- }
- function generateSampleData(location) {
- currentData = {
- 'Fine particles (PM 2.5)': generateTimeSeries(8, 15, -0.02, 2014, 2023),
- 'Ozone (O3)': generateTimeSeries(25, 40, 0.01, 2014, 2023),
- 'Boiler Emissions- Total SO2 Emissions': generateTimeSeries(50, 120, -0.05, 2014, 2023),
- 'Nitrogen dioxide (NO2)': generateTimeSeries(25, 35, -0.015, 2014, 2023),
- 'Boiler Emissions- Total NOx Emissions': generateTimeSeries(150, 250, -0.04, 2014, 2023)
- };
- console.log(`Sample data generated for ${location}.`);
- }
- function generateTimeSeries(min, max, trend, startYear, endYear) {
- const data = [];
- for (let year = startYear; year <= endYear; year++) {
- const baseValue = min + (max - min) * Math.random();
- const trendValue = baseValue * (1 + trend * (year - startYear));
- data.push({ year, value: Math.max(0, trendValue + (Math.random() - 0.5) * (max - min) * 0.1).toFixed(2) });
- }
- return data;
- }
- // --- UI & EVENT HANDLERS ---
- function setupEventListeners() {
- document.getElementById('locationSelect').addEventListener('change', loadHistoricalData);
- document.getElementById('predictBtn').addEventListener('click', runPrediction);
- document.getElementById('viewToggle').addEventListener('change', (e) => {
- currentView = e.target.value;
- toggleChartView();
- });
- const parkingFeeSlider = document.getElementById('parkingFee');
- const feeValueSpan = document.getElementById('feeValue');
- parkingFeeSlider.addEventListener('input', () => feeValueSpan.textContent = parseFloat(parkingFeeSlider.value).toFixed(2));
- const transitFiltersSlider = document.getElementById('transitFilters');
- const filterValueSpan = document.getElementById('filterValue');
- transitFiltersSlider.addEventListener('input', () => filterValueSpan.textContent = transitFiltersSlider.value);
- }
- function setDefaultDates() {
- const today = new Date();
- const nextYear = new Date();
- nextYear.setFullYear(today.getFullYear() + 1);
- document.getElementById('startDate').value = today.toISOString().split('T')[0];
- document.getElementById('endDate').value = nextYear.toISOString().split('T')[0];
- }
- function resetPredictionUI() {
- predictionData = {};
- document.getElementById('predictionsSection').style.display = 'none';
- document.getElementById('historicalDataInfo').style.display = 'none';
- resetPredictionMetrics();
- }
- function resetPredictionMetrics() {
- const metrics = document.querySelectorAll('.metric-value');
- metrics.forEach(metric => metric.textContent = '-');
- }
- // --- PREDICTION LOGIC ---
- function runPrediction() {
- const loadingIndicator = document.getElementById('loadingIndicator');
- const predictBtn = document.getElementById('predictBtn');
- loadingIndicator.style.display = 'block';
- predictBtn.disabled = true;
- // Simulate async AI prediction
- setTimeout(() => {
- generatePredictionData();
- updatePredictionUI();
- updateAllCharts();
- loadingIndicator.style.display = 'none';
- predictBtn.disabled = false;
- document.getElementById('predictionsSection').style.display = 'grid';
- }, 1500);
- }
- function generatePredictionData() {
- predictionData = {};
- const startDate = new Date(document.getElementById('startDate').value);
- const endDate = new Date(document.getElementById('endDate').value);
- const parkingFee = parseFloat(document.getElementById('parkingFee').value);
- const filterCoverage = parseFloat(document.getElementById('transitFilters').value);
- const startYear = startDate.getFullYear();
- const endYear = endDate.getFullYear();
- // More sophisticated reduction factors
- const feeFactor = 1 - (parkingFee - 2) * 0.015; // Higher fee, more reduction
- const filterFactor = 1 - (filterCoverage / 100) * 0.1; // More filters, more reduction
- Object.keys(currentData).forEach(pollutant => {
- const histData = currentData[pollutant];
- if (!histData || histData.length === 0) return;
- const lastKnownValue = parseFloat(histData[histData.length - 1].value);
- const trend = calculateTrend(histData);
- predictionData[pollutant] = [];
- for (let year = startYear; year <= endYear; year++) {
- const yearIndex = year - startYear + 1;
- const predictedValue = lastKnownValue * Math.pow(1 + trend, yearIndex) * feeFactor * filterFactor * (1 + (Math.random() - 0.5) * 0.05);
- predictionData[pollutant].push({ year, value: Math.max(0, predictedValue) });
- }
- });
- }
- function calculateTrend(data) {
- if (data.length < 2) return 0;
- const first = data[0];
- const last = data[data.length - 1];
- const years = last.year - first.year;
- if (years === 0) return 0;
- return (parseFloat(last.value) / parseFloat(first.value) - 1) / years;
- }
- // --- UI UPDATES ---
- function updatePredictionUI() {
- // This is a simplified example. Real-world scenarios would be more complex.
- const filterCoverage = parseFloat(document.getElementById('transitFilters').value);
- // Confidence Metrics
- document.getElementById('pm25Confidence').textContent = `${(88 + Math.random() * 5).toFixed(1)}%`;
- document.getElementById('o3Confidence').textContent = `${(85 + Math.random() * 5).toFixed(1)}%`;
- document.getElementById('so2Confidence').textContent = `${(92 + Math.random() * 4).toFixed(1)}%`;
- document.getElementById('noxConfidence').textContent = `${(90 + Math.random() * 6).toFixed(1)}%`;
- document.getElementById('overallAccuracy').textContent = `${(89 + Math.random() * 3).toFixed(1)}%`;
- // Reductions
- const lastHistoricalPM25 = parseFloat(currentData['Fine particles (PM 2.5)']?.slice(-1)[0]?.value) || 0;
- const firstPredictedPM25 = parseFloat(predictionData['Fine particles (PM 2.5)']?.[0]?.value) || 0;
- if (lastHistoricalPM25 === 0) {
- document.getElementById('pm25Reduction').textContent = 'N/A';
- } else {
- document.getElementById('pm25Reduction').textContent = `${((1 - firstPredictedPM25 / lastHistoricalPM25) * 100).toFixed(1)}%`;
- }
- document.getElementById('o3Reduction').textContent = `${(Math.random() * 5 + 5).toFixed(1)}%`;
- document.getElementById('so2Reduction').textContent = `${(Math.random() * 8 + 10).toFixed(1)}%`;
- document.getElementById('no2Reduction').textContent = `${(Math.random() * 6 + 8).toFixed(1)}%`;
- document.getElementById('noxReduction').textContent = `${(Math.random() * 7 + 9).toFixed(1)}%`;
- // Transit Benefits
- document.getElementById('stationImprovement').textContent = `${(filterCoverage * 0.3).toFixed(1)}%`;
- document.getElementById('busStopReduction').textContent = `${(filterCoverage * 0.1).toFixed(1)}%`;
- document.getElementById('healthBenefit').textContent = `Reduces ${Math.round(filterCoverage * 15)} asthma cases/year`;
- document.getElementById('filterMaintenance').textContent = `$${(filterCoverage * 125000).toLocaleString()}/year`;
- // AQI Improvements
- document.getElementById('currentAQI').textContent = '55 (Moderate)';
- document.getElementById('predictedAQI').textContent = '42 (Good)';
- document.getElementById('healthDays').textContent = `+${Math.round(filterCoverage * 0.5 + 10)} Good AQI Days`;
- document.getElementById('healthSavings').textContent = `$${(Math.random() * 2 + 3).toFixed(1)}M annually`;
- }
- function createPredictionCard(title, metrics, id) {
- const card = document.createElement('div');
- card.className = 'bg-white p-5 rounded-lg shadow-md border-l-4 border-blue-500';
- let metricsHtml = '';
- for (const [label, valueId, valueClass] of metrics) {
- metricsHtml += `
- <div class="flex justify-between items-center py-2 border-b border-gray-200 last:border-b-0">
- <span class="text-sm text-gray-600">${label}</span>
- <span class="text-sm font-bold ${valueClass || 'text-gray-800'}" id="${valueId}">-</span>
- </div>
- `;
- }
- card.innerHTML = `
- <h3 class="text-lg font-bold text-gray-800 mb-3">${title}</h3>
- <div id="${id}">
- ${metricsHtml}
- </div>
- `;
- return card;
- }
- function createIndividualChartContainers() {
- const container = document.getElementById('individualChartsContainer');
- container.innerHTML = ''; // Clear existing
- Object.keys(POLLUTANT_CONFIG).forEach(key => {
- const config = POLLUTANT_CONFIG[key];
- const shortName = key.match(/^(.*?)\s\(/)?.[1] || key;
- const chartDiv = document.createElement('div');
- // FIX: Added fixed height and flexbox layout to prevent infinite resize bug.
- chartDiv.className = 'bg-white p-4 rounded-lg shadow-md h-[400px] flex flex-col';
- chartDiv.innerHTML = `
- <div class="text-center font-bold text-gray-700 mb-2 flex-shrink-0">${shortName}</div>
- <div class="relative flex-grow">
- <canvas id="${config.id}"></canvas>
- </div>
- `;
- container.appendChild(chartDiv);
- });
- // Create prediction cards
- const predictionsContainer = document.getElementById('predictionsSection');
- predictionsContainer.innerHTML = ''; // Clear
- predictionsContainer.appendChild(createPredictionCard('🎯 Prediction Confidence', [
- ['PM2.5 Confidence', 'pm25Confidence', 'text-yellow-600'],
- ['O₃ Confidence', 'o3Confidence', 'text-yellow-600'],
- ['SO₂ Confidence', 'so2Confidence', 'text-yellow-600'],
- ['NOx Confidence', 'noxConfidence', 'text-yellow-600'],
- ['Overall Accuracy', 'overallAccuracy', 'text-yellow-600 font-extrabold']
- ], 'confidenceMetrics'));
- predictionsContainer.appendChild(createPredictionCard('📉 Pollutant Reductions', [
- ['PM2.5 Reduction', 'pm25Reduction', 'text-green-600'],
- ['O₃ Reduction', 'o3Reduction', 'text-green-600'],
- ['SO₂ Reduction', 'so2Reduction', 'text-green-600'],
- ['NO₂ Reduction', 'no2Reduction', 'text-green-600'],
- ['NOx Reduction', 'noxReduction', 'text-green-600']
- ], 'reductionMetrics'));
- predictionsContainer.appendChild(createPredictionCard('🚇 Transit Filter Benefits', [
- ['Station Air Quality', 'stationImprovement', 'text-green-600'],
- ['Bus Stop Pollution', 'busStopReduction', 'text-green-600'],
- ['Public Health Benefit', 'healthBenefit', 'text-green-600'],
- ['Annual Maintenance', 'filterMaintenance', 'text-gray-800']
- ], 'transitMetrics'));
- predictionsContainer.appendChild(createPredictionCard('🌟 AQI Improvements', [
- ['Current Avg AQI', 'currentAQI', 'text-gray-800'],
- ['Predicted Avg AQI', 'predictedAQI', 'text-green-600'],
- ['Health Days Improved', 'healthDays', 'text-green-600'],
- ['Est. Health Savings', 'healthSavings', 'text-green-600 font-extrabold']
- ], 'aquiMetrics'));
- }
- // --- CHARTING ---
- function initializeAllCharts() {
- // Combined Chart
- const ctxCombined = document.getElementById('airQualityChart').getContext('2d');
- airQualityChart = new Chart(ctxCombined, getChartOptions('NYC Core Air Quality Analysis', 'Concentration (Various Units)'));
- // Individual Charts
- Object.keys(POLLUTANT_CONFIG).forEach(key => {
- const config = POLLUTANT_CONFIG[key];
- const ctx = document.getElementById(config.id).getContext('2d');
- individualCharts[key] = new Chart(ctx, getChartOptions(key, `Concentration (${config.unit})`));
- });
- }
- function getChartOptions(titleText, yAxisLabel) {
- return {
- type: 'line',
- data: { labels: [], datasets: [] },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- title: { display: false }, // Using custom titles in HTML
- legend: { position: 'top', labels: { usePointStyle: true, padding: 20 } },
- tooltip: { mode: 'index', intersect: false, backgroundColor: 'rgba(0,0,0,0.8 )' }
- },
- scales: {
- x: { title: { display: true, text: 'Year', font: { weight: 'bold' } } },
- y: { title: { display: true, text: yAxisLabel, font: { weight: 'bold' } } }
- },
- interaction: { mode: 'nearest', axis: 'x', intersect: false }
- }
- };
- }
- function updateAllCharts() {
- if (currentView === 'combined') {
- updateCombinedChart();
- } else {
- updateIndividualCharts();
- }
- }
- function toggleChartView() {
- const combinedContainer = document.getElementById('combinedChartContainer');
- const individualContainer = document.getElementById('individualChartsContainer');
- if (currentView === 'combined') {
- combinedContainer.style.display = 'block';
- individualContainer.style.display = 'none';
- updateCombinedChart();
- } else {
- combinedContainer.style.display = 'none';
- individualContainer.style.display = 'grid';
- updateIndividualCharts();
- }
- }
- function updateCombinedChart() {
- const allYears = new Set();
- Object.values(currentData).flat().forEach(d => allYears.add(d.year));
- Object.values(predictionData).flat().forEach(d => allYears.add(d.year));
- const labels = Array.from(allYears).sort();
- airQualityChart.data.labels = labels;
- airQualityChart.data.datasets = Object.keys(currentData).map(pollutant => {
- const config = POLLUTANT_CONFIG[pollutant];
- const histData = currentData[pollutant] || [];
- const predData = predictionData[pollutant] || [];
- const shortName = pollutant.match(/^(.*?)\s\(/)?.[1] || pollutant;
- const dataPoints = labels.map(year => {
- const histPoint = histData.find(d => d.year === year);
- return histPoint ? histPoint.value : null;
- });
- if (predData.length > 0) {
- // Bridge the gap between historical and prediction
- dataPoints[labels.indexOf(predData[0].year) - 1] = histData.slice(-1)[0]?.value;
- predData.forEach(p => {
- const idx = labels.indexOf(p.year);
- if (idx !== -1) dataPoints[idx] = p.value;
- });
- }
- return {
- label: shortName,
- data: dataPoints,
- borderColor: config.color,
- backgroundColor: `${config.color}33`, // a little transparency
- tension: 0.1,
- spanGaps: true
- };
- });
- airQualityChart.update();
- }
- function updateIndividualCharts() {
- Object.keys(individualCharts).forEach(pollutant => {
- const chart = individualCharts[pollutant];
- const config = POLLUTANT_CONFIG[pollutant];
- const histData = currentData[pollutant] || [];
- const predData = predictionData[pollutant] || [];
- const allYears = new Set([...histData.map(d => d.year), ...predData.map(d => d.year)]);
- const labels = Array.from(allYears).sort();
- chart.data.labels = labels;
- const datasets = [];
- // Historical Data
- datasets.push({
- label: 'Historical',
- data: labels.map(year => histData.find(d => d.year === year)?.value || null),
- borderColor: config.color,
- backgroundColor: `${config.color}33`,
- tension: 0.1,
- spanGaps: false
- });
- // Prediction Data
- if (predData.length > 0) {
- const predictionPoints = labels.map(year => predData.find(d => d.year === year)?.value || null);
- // Bridge the gap
- const bridgeIndex = labels.indexOf(predData[0].year);
- if (bridgeIndex > 0) {
- predictionPoints[bridgeIndex - 1] = histData.find(d => d.year === labels[bridgeIndex - 1])?.value || null;
- }
- datasets.push({
- label: 'Predicted',
- data: predictionPoints,
- borderColor: config.color,
- borderDash: [5, 5],
- backgroundColor: `${config.color}1A`,
- tension: 0.1,
- spanGaps: true
- });
- }
- chart.data.datasets = datasets;
- chart.update();
- });
- }
- // --- EXPORT FUNCTIONS ---
- function exportToPDF() {
- const { jsPDF } = window.jspdf;
- const doc = new jsPDF();
- doc.setFontSize(18);
- doc.text("NYC Air Quality Analysis Report", 14, 22);
- doc.setFontSize(11);
- doc.setTextColor(100);
- doc.text(`Location: ${document.getElementById('locationSelect').value}`, 14, 30);
- const canvas = document.getElementById(currentView === 'combined' ? 'airQualityChart' : Object.values(POLLUTANT_CONFIG)[0].id);
- const imgData = canvas.toDataURL('image/png');
- doc.addImage(imgData, 'PNG', 14, 40, 180, 100);
- let tableData = [];
- const predictionCards = document.querySelectorAll('#predictionsSection > div');
- predictionCards.forEach(card => {
- const title = card.querySelector('h3').innerText;
- doc.autoTable({
- head: [[title]],
- body: Array.from(card.querySelectorAll('.flex.justify-between')).map(row => [row.children[0].innerText, row.children[1].innerText]),
- startY: doc.previousAutoTable.finalY + 10 || 150,
- headStyles: { fillColor: [59, 130, 246] }
- });
- });
- doc.save(`air_quality_report_${new Date().toISOString().split('T')[0]}.pdf`);
- }
- function exportToCSV() {
- let csvContent = "data:text/csv;charset=utf-8,";
- csvContent += "Pollutant,Year,Value,Type\n";
- const dataToExport = { ...currentData, ...predictionData };
- Object.keys(dataToExport).forEach(pollutant => {
- if (currentData[pollutant]) {
- currentData[pollutant].forEach(row => {
- csvContent += `${pollutant},${row.year},${row.value},Historical\n`;
- });
- }
- if (predictionData[pollutant]) {
- predictionData[pollutant].forEach(row => {
- csvContent += `${pollutant},${row.year},${row.value},Predicted\n`;
- });
- }
- });
- const encodedUri = encodeURI(csvContent);
- const link = document.createElement("a");
- link.setAttribute("href", encodedUri);
- link.setAttribute("download", "air_quality_data.csv");
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- }
- function exportChart() {
- const canvas = document.getElementById(currentView === 'combined' ? 'airQualityChart' : Object.values(POLLUTANT_CONFIG)[0].id);
- const link = document.createElement('a');
- link.download = `air_quality_chart_${currentView}.png`;
- link.href = canvas.toDataURL('image/png');
- link.click();
- }
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment