import gradio as gr import requests import pandas as pd import folium from folium import plugins import json from datetime import datetime, timedelta import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots class AirQualityMonitor: def __init__(self): self.base_url = "https://www.airnowapi.org/aq" # You'll need to get an API key from https://docs.airnowapi.org/ self.api_key = "YOUR_API_KEY_HERE" # Replace with actual API key def get_current_observations(self, state_code=None, bbox=None): """Get current air quality observations""" if not self.api_key or self.api_key == "YOUR_API_KEY_HERE": # Return sample data for demo purposes return self._get_sample_data() url = f"{self.base_url}/observation/zipCode/current/" params = { 'format': 'application/json', 'API_KEY': self.api_key, 'distance': 100 } if state_code: # Get observations for a specific state params['zipCode'] = self._get_state_zip(state_code) try: response = requests.get(url, params=params) response.raise_for_status() return response.json() except Exception as e: print(f"API Error: {e}") return self._get_sample_data() def _get_state_zip(self, state_code): """Get a representative zip code for a state""" state_zips = { 'CA': '90210', 'NY': '10001', 'TX': '73301', 'FL': '33101', 'IL': '60601', 'PA': '19101', 'OH': '43215', 'GA': '30301', 'NC': '27601', 'MI': '48201', 'NJ': '07001', 'VA': '23219', 'WA': '98101', 'AZ': '85001', 'MA': '02101', 'TN': '37201', 'IN': '46201', 'MO': '63101', 'MD': '21201', 'WI': '53201' } return state_zips.get(state_code, '90210') def _get_sample_data(self): """Return sample data for demonstration""" return [ { "DateObserved": "2025-06-29", "HourObserved": 14, "LocalTimeZone": "PST", "ReportingArea": "Los Angeles-South Coast Air Basin", "StateCode": "CA", "Latitude": 34.0522, "Longitude": -118.2437, "ParameterName": "PM2.5", "AQI": 65, "Category": {"Number": 2, "Name": "Moderate"} }, { "DateObserved": "2025-06-29", "HourObserved": 14, "LocalTimeZone": "EST", "ReportingArea": "New York", "StateCode": "NY", "Latitude": 40.7128, "Longitude": -74.0060, "ParameterName": "Ozone", "AQI": 45, "Category": {"Number": 1, "Name": "Good"} }, { "DateObserved": "2025-06-29", "HourObserved": 14, "LocalTimeZone": "CST", "ReportingArea": "Chicago", "StateCode": "IL", "Latitude": 41.8781, "Longitude": -87.6298, "ParameterName": "PM2.5", "AQI": 85, "Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"} }, { "DateObserved": "2025-06-29", "HourObserved": 14, "LocalTimeZone": "MST", "ReportingArea": "Phoenix", "StateCode": "AZ", "Latitude": 33.4484, "Longitude": -112.0740, "ParameterName": "Ozone", "AQI": 95, "Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"} }, { "DateObserved": "2025-06-29", "HourObserved": 14, "LocalTimeZone": "PST", "ReportingArea": "Seattle", "StateCode": "WA", "Latitude": 47.6062, "Longitude": -122.3321, "ParameterName": "PM2.5", "AQI": 25, "Category": {"Number": 1, "Name": "Good"} }, { "DateObserved": "2025-06-29", "HourObserved": 14, "LocalTimeZone": "EST", "ReportingArea": "Miami", "StateCode": "FL", "Latitude": 25.7617, "Longitude": -80.1918, "ParameterName": "Ozone", "AQI": 55, "Category": {"Number": 2, "Name": "Moderate"} } ] def get_aqi_color(aqi): """Return color based on AQI value""" if aqi <= 50: return "#00E400" # Green elif aqi <= 100: return "#FFFF00" # Yellow elif aqi <= 150: return "#FF7E00" # Orange elif aqi <= 200: return "#FF0000" # Red elif aqi <= 300: return "#8F3F97" # Purple else: return "#7E0023" # Maroon def create_air_quality_map(data): """Create an interactive map with air quality data""" if not data: # Create empty map centered on US m = folium.Map(location=[39.8283, -98.5795], zoom_start=4) folium.Marker( [39.8283, -98.5795], popup="No data available. Please add your AirNow API key.", icon=folium.Icon(color='red') ).add_to(m) return m._repr_html_() # Calculate map center lats = [float(station['Latitude']) for station in data] lons = [float(station['Longitude']) for station in data] center_lat = sum(lats) / len(lats) center_lon = sum(lons) / len(lons) # Create map m = folium.Map(location=[center_lat, center_lon], zoom_start=4) # Add markers for each monitoring station for station in data: lat = float(station['Latitude']) lon = float(station['Longitude']) aqi = station.get('AQI', 0) parameter = station.get('ParameterName', 'Unknown') area = station.get('ReportingArea', 'Unknown Area') category = station.get('Category', {}).get('Name', 'Unknown') color = get_aqi_color(aqi) popup_html = f"""

{area}

AQI: {aqi}

Parameter: {parameter}

Category: {category}

Date: {station.get('DateObserved', 'N/A')}

Time: {station.get('HourObserved', 'N/A')}:00 {station.get('LocalTimeZone', '')}

""" folium.CircleMarker( location=[lat, lon], radius=8, popup=folium.Popup(popup_html, max_width=250), color='black', weight=1, fill=True, fillColor=color, fillOpacity=0.8, tooltip=f"{area}: AQI {aqi}" ).add_to(m) # Add legend legend_html = '''

AQI Legend

Good (0-50)

Moderate (51-100)

Unhealthy for Sensitive (101-150)

Unhealthy (151-200)

Very Unhealthy (201-300)

''' m.get_root().html.add_child(folium.Element(legend_html)) return m._repr_html_() def create_aqi_chart(data): """Create a chart showing AQI levels by location""" if not data: return None df = pd.DataFrame(data) df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce') # Create bar chart fig = px.bar(df, x='ReportingArea', y='AQI', color='AQI', color_continuous_scale='RdYlGn_r', title='Air Quality Index by Location', labels={'AQI': 'Air Quality Index', 'ReportingArea': 'Location'}) fig.update_layout(xaxis_tickangle=-45, height=500) fig.update_traces(hovertemplate='%{x}
AQI: %{y}') return fig def get_air_quality_summary(data): """Generate a summary of air quality data""" if not data: return "No air quality data available." df = pd.DataFrame(data) df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce') total_stations = len(df) avg_aqi = df['AQI'].mean() max_aqi = df['AQI'].max() min_aqi = df['AQI'].min() # Count by category categories = df['Category'].apply(lambda x: x.get('Name', 'Unknown') if isinstance(x, dict) else 'Unknown') category_counts = categories.value_counts() summary = f""" ## Air Quality Summary **Total Monitoring Stations:** {total_stations} **Average AQI:** {avg_aqi:.1f} **Highest AQI:** {max_aqi} **Lowest AQI:** {min_aqi} ### Air Quality Categories: """ for category, count in category_counts.items(): percentage = (count / total_stations) * 100 summary += f"- **{category}:** {count} stations ({percentage:.1f}%)\n" return summary def update_data(): """Main function to fetch and display air quality data""" monitor = AirQualityMonitor() data = monitor.get_current_observations() map_html = create_air_quality_map(data) chart = create_aqi_chart(data) summary = get_air_quality_summary(data) return map_html, chart, summary # Create Gradio interface with gr.Blocks(title="Air Quality Monitor", theme=gr.themes.Soft()) as demo: gr.Markdown("# 🌬️ Real-Time Air Quality Monitor") gr.Markdown("Monitor air quality data from NOAA/EPA AirNow stations across the United States") with gr.Row(): gr.Markdown(""" This application displays real-time air quality data from over 2,000 monitoring stations across the United States. The data includes Air Quality Index (AQI) values for various pollutants like PM2.5, PM10, Ozone, and more. **Note:** To access live data, you need to: 1. Get a free API key from [AirNow API](https://docs.airnowapi.org/) 2. Replace `YOUR_API_KEY_HERE` in the code with your actual API key Currently showing sample data for demonstration. """) with gr.Row(): update_btn = gr.Button("🔄 Update Air Quality Data", variant="primary", size="lg") with gr.Row(): with gr.Column(scale=2): map_output = gr.HTML(label="Air Quality Map") with gr.Column(scale=1): summary_output = gr.Markdown(label="Summary") with gr.Row(): chart_output = gr.Plot(label="AQI Chart") # Add information section with gr.Row(): gr.Markdown(""" ## About Air Quality Index (AQI) The AQI is an index for reporting daily air quality. It tells you how clean or polluted your air is: - **Good (0-50)**: 🟢 Air quality is satisfactory - **Moderate (51-100)**: 🟡 Acceptable for most people - **Unhealthy for Sensitive Groups (101-150)**: 🟠 May cause problems for sensitive individuals - **Unhealthy (151-200)**: 🔴 May cause health problems for everyone - **Very Unhealthy (201-300)**: 🟣 Health alert for everyone - **Hazardous (301+)**: 🟤 Health emergency **Data Source:** EPA AirNow - Real-time air quality observations from government monitoring stations """) # Load initial data demo.load(update_data, outputs=[map_output, chart_output, summary_output]) update_btn.click(update_data, outputs=[map_output, chart_output, summary_output]) if __name__ == "__main__": demo.launch()