CE105 / app.py
isabellegoebel's picture
Update app.py
1c64e70 verified
import dash
from dash import dcc, html, Input, Output, State, callback_context
import plotly.express as px
import matplotlib.pyplot as plt
import numpy as np
import io
import base64
from datetime import datetime, timedelta
import matplotlib
matplotlib.use('Agg') # Server-safe backend
from herbie import Herbie
from herbie.toolbox import EasyMap, pc
from herbie import paint
import cartopy.crs as ccr
from package.hrrr_smoke_app import *
from package.school_alerts import *
from package.tropomi_app import *
app = dash.Dash(__name__, suppress_callback_exceptions=True)
server = app.server
# Load school alert system plots
shafter, bakersfield = load_and_clean_data("package/Shafter.csv", "package/Bakersfield.csv", col1='pm25_avg_60', col2='pm25')
delano, porterville = load_and_clean_data("package/Delano.csv", "package/Porterville.csv")
fig1 = plot_combined_line(shafter, bakersfield, "Shafter", "Bakersfield", col1='pm25_avg_60', col2='pm25')
fig2 = plot_combined_line(delano, porterville, "Delano", "Porterville")
fig3 = plot_alert_comparison_bar(shafter, bakersfield, "Shafter", "Bakersfield", col1='pm25_avg_60', col2='pm25')
fig4 = plot_alert_comparison_bar(delano, porterville, "Delano", "Porterville")
# === PAGE LAYOUTS ===
def home_page():
return html.Div([
html.Div([
html.H1("SJVAir Community Dashboard", style={"color": "white", "marginBottom": "10px"}),
html.P("Your portal for air quality insights, smoke forecasts, and local resources.",
style={"color": "white", "fontSize": "18px"}),
html.Div([
html.Button(
dcc.Link("Smoke Forecast", href="/hrrr-smoke", className="nav-link"),
style={
"backgroundColor": "#1fb9ef", "color": "white", "border": "none",
"padding": "12px 24px", "borderRadius": "8px", "fontSize": "16px",
"boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"
}),
html.Button(
dcc.Link("Trajectory Model", href="/hysplit", className="nav-link"),
style={
"backgroundColor": "#1fb9ef", "color": "white", "border": "none",
"padding": "12px 24px", "borderRadius": "8px", "fontSize": "16px",
"boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"
}),
html.Button(
dcc.Link("Pollutants", href="/airtrends", className="nav-link"),
style={
"backgroundColor": "#1fb9ef", "color": "white", "border": "none",
"padding": "12px 24px", "borderRadius": "8px", "fontSize": "16px",
"boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"
})
], style={"display": "flex", "justifyContent": "center", "gap": "10px"})
], style={
"backgroundColor": "#3b4b6b", "padding": "50px", "textAlign": "center",
"boxShadow": "0 4px 8px rgba(0, 0, 0, 0.1)", "marginBottom": "20px"
}),
html.Div([
html.Div([
html.H2("Mission Statement", style={"color": "#1f2a44"}),
html.P("Working to empower communities with the tools, knowledge, and support to monitor and improve air quality, advocate for healthier environments, and drive meaningful policy changes for a better future."),
html.H2("What's in the Air?", style={"color": "#1f2a44"}),
html.P("Track smoke plumes, view HRRR forecasts, and stay informed about current conditions in your area."),
html.Ul([
html.Li("There’s a wildfire happening. Where is the smoke coming from, and when will it affect you? Use the HRRR-Smoke page."),
html.Li("You smell something unusual. Where is it coming from? Plot the trajectory on the HYSPLIT page."),
html.Li("The air quality looks poor. What is in it? Look at the current pollutants on the air trends page."),
], style={"marginTop": "15px", "lineHeight": "1.8",}),
html.H2("How can I get involved?", style={"color": "#1f2a44"}),
html.P("Visit the community resources page!"),
], style={"width": "45%", "padding": "40px", "color": "#4C4E52"}),
html.Iframe(
src="/assets/homepage_infographics.html",
style={
"width": "45%",
"height": "1000px",
"border": "none",
"marginTop": "20px",
"marginBottom": "40px",
}
)
], style={"display": "flex", "justifyContent": "center", "gap": "20px", "padding": "0px"}),
html.Div([
html.H3("CE 105, UC Berkeley, May 2025", style={"color": "white", "marginBottom": "10px"}),
html.P("Isabelle Goebel, Jugveer Singh, Alexandre Santiago Vasquez, Itzel Gonzalez", style={"color": "white", "marginBottom": "10px"}),
html.H3("References, Acknowledgements, and Data Sources", style={"color": "white", "marginBottom": "10px"}),
html.P("We are grateful to the following organizations for providing us with the data and models that support this site:", style={"color": "white", "fontSize": "14px"}),
html.Ul([
html.Li(
html.A("SJVAir: Community partner providing real-time air quality data and insights into our project.",
href="https://www.sjvair.com/",
target="_blank",
style={"color": "#99ccff"}),
style={"color": "white", "fontSize": "14px"}),
html.P(html.I("Thank you to SJVAir for collaborating with us to bring these models and data to the community."), style={"color": "white", "fontSize": "14px", "marginBottom": "10px"})
]),
html.Ul([
html.Li("NOAA (National Oceanic and Atmospheric Administration): Provider of pollutant and smoke tracking models.", style={"color": "white", "fontSize": "14px"}),
html.Ul([
html.Li([
html.A("HYSPLIT: Hybrid Single-Particle Lagrangian Integrated Trajectory model",
href="https://www.ready.noaa.gov/HYSPLIT.php",
target="_blank",
style={"color": "#99ccff"})
], style={"color": "white", "fontSize": "14px"}),
html.Li([
html.A("HRRR: High-Resolution Rapid Refresh model",
href="https://rapidrefresh.noaa.gov/hrrr/",
target="_blank",
style={"color": "#99ccff"})
], style={"color": "white", "fontSize": "14px"}),
html.Li([
html.A("Accessed via Herbie, a Python package for HRRR data retrieval",
href="https://github.com/blaylockbk/Herbie",
target="_blank",
style={"color": "#99ccff"}),
], style={"color": "white", "fontSize": "14px", "marginBottom": "10px"}),
])]),
html.Ul([
html.Li("ESA & Copernicus Data Space Ecosystem: Provider of satellite data.", style={"color": "white", "fontSize": "14px"}),
html.Ul([
html.Li([
html.A("TROPOMI (TROPOspheric Monitoring Instrument): Satellite instrument on board the Sentinel-5P used for measuring air quality",
href="https://www.google.com/url?q=https://documentation.dataspace.copernicus.eu/APIs/SentinelHub/Process/Examples/S5PL2.html&sa=D&source=docs&ust=1745538372397012&usg=AOvVaw2b8UJS9HgxaC3RWeCl_mKK",
target="_blank",
style={"color": "#99ccff"})
], style={"color": "white", "fontSize": "14px", "marginBottom": "10px"})
])]),
html.Ul([
html.Li(
html.A("Guidelines related to deposition of particulate matter.",
href="https://pmc.ncbi.nlm.nih.gov/articles/PMC6013115/",
target="_blank",
style={"color": "#99ccff"})),
html.Li(
html.A("Community Emissions Reduction Program: Stockton.",
href="https://community.valleyair.org/media/2487/final-stockton-cerp-no-appendix-with-cover.pdf",
target="_blank",
style={"color": "#99ccff"}))
], style={"color": "white", "fontSize": "14px"}),
], style={"backgroundColor": "#3b4b6b", "padding": "60px", "textAlign": "left"}),
], style={"fontFamily": "Inter, Roboto, Arial, sans-serif"})
def hrrr_smoke_page():
return html.Div([
html.Div([
html.H1("HRRR-Smoke", style={"color": "black"}),
], style={"backgroundColor": "#ffffff", "padding": "30px", "textAlign": "left", "boxShadow": "0 2px 8px rgba(0, 0, 0, 0.05)"}),
html.Div([
html.Div([
html.H2("Introduction", style={"color": "#1f2a44"}),
html.P("We want to help the community advocate for their health, especially regarding the effects of wildfire smoke. One important tool is the HRRR-Smoke model, which predicts where wildfire smoke will move and how concentrated it will be. By showing smoke plumes in real time, HRRR-Smoke helps people understand when and where the smoke will spread. This information is vital for individuals to take steps to protect their health.", style={"color": "#4C4E52"}),
html.H2("Instructions", style={"color": "#1f2a44", "marginTop": "20px"}),
html.P("To get information about smoke levels, enter the date you're interested in, whether it’s in the past, present, or future (up to 48 hours). The model will create a smoke plot for that date, showing expected smoke levels. HRRR-Smoke can help you understand current wildfires or make future predictions. You can also download the plot using the button below.", style={"color": "#4C4E52"})
], style={"width": "50%", "padding": "20px"}),
html.Div([
html.Iframe(
src="/assets/hrrr_video.html",
style={"width": "100%", "height": "400px", "border": "none"}
),
html.P("Introduction and instructions video. The Spanish version starts at 0:44.",
style={"textAlign": "center", "marginTop": "0px", "color": "#4C4E52"})
], style={"width": "65%", "padding": "20px", "display": "flex", "flexDirection": "column"})
], style={"display": "flex", "flexDirection": "row", "padding": "20px"}),
html.Hr(style={'border': 'none', 'height': '0.5px', 'backgroundColor': 'lightgrey', 'margin': '0'}),
html.Div([
html.H2("Select a Date and Time", style={"color": "#1f2a44"}),
html.Label("Select Date", style={"color": "#1f2a44", "marginBottom": "0px"}),
dcc.DatePickerSingle(
id='date-picker',
date=(datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d'),
max_date_allowed=datetime.now().date(),
display_format='YYYY-MM-DD',
style={
'padding': '12px',
'backgroundColor': '#ffffff',
'fontFamily': 'Inter',
'border': '1px solid #ccc',
'borderRadius': '8px',
'boxShadow': "0px 4px 6px rgba(0, 0, 0, 0.1)",
'width': '130px',
'fontSize': '16px'
}
),
html.Div(style={'height': '16px'}),
html.Label("Select Hour (UTC)", style={"color": "#1f2a44", "marginTop": "10px", "marginBottom": "0px"}),
dcc.Dropdown(
id='hour-picker',
options=[{'label': f'{h:02d}:00', 'value': h} for h in range(24)],
value=0,
clearable=False,
style={
'backgroundColor': '#ffffff',
'fontFamily': 'Inter',
'boxShadow': "0px 4px 6px rgba(0, 0, 0, 0.1)",
'borderRadius': '8px',
'fontSize': '16px',
'width': '156px',
'textAlign': 'center',
}),
html.Div(style={'height': '16px'}),
html.Label("Optional: Select Forecast (Hours) to View Future Data, Up to 48 Hours", style={"color": "#1f2a44", "marginTop": "10px", "marginBottom": "0px"}),
dcc.Input(
id='forecast-picker',
type='number',
min=0,
max=48,
value=0,
style={
'backgroundColor': '#ffffff',
'boxShadow': "0px 4px 6px rgba(0, 0, 0, 0.1)",
'fontSize': '16px',
'fontFamily': 'Inter',
'border': '1px solid #ccc',
'borderRadius': '8px',
'width': '140px',
'textAlign': 'center',
'padding': '7px'
})
], style={"padding": "40px", "display": "flex", "flexDirection": "column", "gap": "5px", "maxWidth": "600px"}),
html.Hr(style={'border': 'none', 'height': '1px', 'backgroundColor': 'lightgrey', 'margin': '0'}),
html.Div([
html.H2("Forecast Visualization", style={"color": "#1f2a44", "marginBottom": "10px"}),
html.P("Below is the smoke forecast map based on your selected date and time.", style={"color": "#4C4E52"}),
dcc.Loading(
id="loading-plot",
type="circle",
color="#1f2a44",
children=html.Div([
dcc.Graph(
id='plotly-graph',
className='responsive-graph',
config={
"displayModeBar": True,
"responsive": True,
}),
dcc.Download(id="download-plot"),
], style={
"textAlign": "center",
"padding": "20px"
})
)
], style={"backgroundColor": "#f0f0f0", "padding": "40px", "textAlign": "center", "border-radius": "10px"})
], style={"fontFamily": "Inter, Roboto, Arial, sans-serif"})
def hysplit_page():
return html.Div([
html.Div([
html.H1("HYSPLIT", style={"color": "black"}),
], style={
"backgroundColor": "#ffffff",
"padding": "30px",
"textAlign": "left",
"boxShadow": "0 2px 8px rgba(0, 0, 0, 0.05)"
}),
html.Div([
html.Div([
html.Div([
html.H2("Purpose", style={"color": "#1f2a44"}),
html.P(
"Trajectory modeling in HYSPLIT calculates the path that air parcels follow through the atmosphere over time. It can track where pollutants originated using backward trajectories and predict where they will go with forward trajectories. This tool is also used to trace the movement of dust, smoke, volcanic ash, and radioactive particles.",
style={"color": "#4C4E52"}
)
], style={"padding": "20px"}),
html.Div([
html.H2("Process", style={"color": "#1f2a44"}),
html.P(
"HYSPLIT uses meteorological data such as wind speed and direction, temperature, date, time, and the location of the start or end point to model air particle movement. It also considers the elevation of the initial or final air pollutant and traces the motion of air parcels in two-dimensional space.",
style={"color": "#4C4E52"}
)
], style={"padding": "20px"}),
html.Div([
html.H2("Instructions", style={"color": "#1f2a44"}),
html.P(
"To begin, please click a point on the map or manually enter the latitude and longitude for your start position. Next, choose a start date and start hour. Then, select a meteorological model (or leave as default). Choose a starting height for your pollutant to start at (or leave as default). Click run, and a trajectory will display on the map. Feel free to click clear and start over.",
style={"color": "#4C4E52"}
)
], style={"padding": "20px"}),
html.Div([
html.Iframe(
src="/assets/hysplit_video.html",
style={
"width": "100%",
"border": "none",
}
)
], style={"padding": "20px"}),
], style={"display": "flex", "flexDirection": "column", "flex": "1", "padding": "20px"}),
html.Div([
html.Iframe(
src="/assets/hysplit_infographic.html",
style={
"width": "100%",
"border": "none"
}
)
], style={"flex": "1", "padding": "20px"})
], style={"display": "flex", "flexDirection": "row", "alignItems": "flex-start"}),
html.Hr(style={'border': 'none', 'height': '1px', 'backgroundColor': 'lightgrey', 'margin': '0'}),
html.Iframe(
src="/assets/hysplit_trajectory.html",
style={
"width": "100%",
"height": "1000px",
"border": "none",
"marginTop": "0px",
})
], style={"fontFamily": "Inter, Roboto, Arial, sans-serif"})
def air_trends_page():
return html.Div([
html.Div([
html.H1("Air Quality Trends", style={"color": "black"}),
], style={
"backgroundColor": "#ffffff",
"padding": "30px",
}),
html.Div([
html.H2("TROPOMI", style={"color": "#1f2a44"}),
html.P("TROPOMI is a satellite that measures atmospheric gases and aerosols. It can measure various pollutants, including nitrogen dioxide (NO2), ozone (O3), carbon monoxide (CO), sulfur dioxide (SO2), and methane (CH4). We can use this to visualize how policies are affecting the air quality over several years, such as sulfur dioxide from the burning of fossil fuels or nitrogen dioxide from diesel fuel. To get started, select a pollutant and date (beginning in 2019 or later). High values will be shown in orange and red.", style={"color": "#4C4E52"})
], style={"padding": "40px 40px 0px 40px"}),
html.Div([
html.H3("Select a Pollutant and Date", style={"color": "#1f2a44"}),
html.Label("Select Pollutant", style={"color": "#1f2a44"}),
dcc.Dropdown(
id="tropomi-pollutant-dropdown",
options=[
{"label": "CO", "value": "CO"},
{"label": "NO2", "value": "NO2"},
{"label": "O3", "value": "O3"},
{"label": "SO2", "value": "SO2"},
{"label": "CH4", "value": "CH4"},
{"label": "AER AI 340 and 380", "value": "aer_ai_340_380"},
{"label": "AER AI 354 and 388", "value": "aer_ai_354_388"},
],
value="CO",
style={
'backgroundColor': '#ffffff',
'fontFamily': 'Inter',
'boxShadow': "0px 4px 6px rgba(0, 0, 0, 0.05)",
'borderRadius': '8px',
'fontSize': '16px',
'width': '200px',
'textAlign': 'center',
}
),
html.Label("Select Date", style={"color": "#1f2a44", 'marginTop': '20px'}),
dcc.DatePickerSingle(
id='tropomi-date-picker',
date=(datetime.now() - timedelta(days=2)).strftime('%Y-%m-%d'),
display_format='YYYY-MM-DD',
max_date_allowed=datetime.now().date(),
style={
'marginBottom': '20px',
'padding': '12px',
'backgroundColor': '#ffffff',
'fontFamily': 'Inter',
'border': '1px solid #ccc',
'borderRadius': '8px',
'boxShadow': "0px 4px 6px rgba(0, 0, 0, 0.1)",
'width': '130px',
'fontSize': '16px'
}
),
dcc.Loading(
id="tropomi-loading",
type="circle",
color="#1f2a44",
children=html.Div(id='tropomi-map-container'),
fullscreen=False
)
], style={"padding": "0px 40px 40px 40px", "justifyContent": "left", "alignItems": "left", "display": "flex", "flexDirection": "column", "gap": "5px", "maxWidth": "600px"}),
], style={"fontFamily": "Inter, Roboto, Arial, sans-serif"})
def restaurants():
return html.Div([
html.Div([
html.H1("Restaurant Emissions", style={"color": "black"}),
], style={
"backgroundColor": "#ffffff",
"padding": "30px",
}),
html.Div([
html.H2("Cooking and Charbroiling", style={"color": "#1f2a44"}),
html.P("Cooking, including charbroiling accounts for over 25% of directly-emitted PM2.5 pollution in Stockton (CERP). These maps show where particulates emitted from the restaurants would land. This was done using HYSPLIT’s dispersion model. After selecting a restaurant’s coordinates as a source location, an archived meteorological file was selected. Five meteorological files were selected for their varying wind direction. The five restaurants were chosen for their placement in South Stockton and between the I-5 and SR 99. The darker blue represents a higher concentration of particles touching down in that area.", style={"color": "#4C4E52"}),
], style={"padding": "40px 40px 0 40px"}),
html.Div([
html.Div([
html.Div([
html.Iframe(
src="/assets/arcgis_embed_1.html",
style={
"width": "100%",
"height": "500px",
"border": "none",
"marginTop": "0px",
"boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"}),
html.P("Deposition of emitted particles when the wind blows from the East for the selected 5 restaurants on Dr Martin Luther King Jr Blvd on December 7th, 2024.", style=text_style),
], style=cell_style),
html.Div([
html.Iframe(
src="/assets/arcgis_embed_2.html",
style={
"width": "100%",
"height": "500px",
"border": "none",
"marginTop": "0px",
"boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"}),
html.P("Deposition of emitted particles when the wind blows from the West for the selected 5 restaurants on Dr Martin Luther King Jr Blvd on March 29th, 2025.", style=text_style),
], style=cell_style),
], style=row_style),
html.Div([
html.Div([
html.Iframe(
src="/assets/arcgis_embed_3.html",
style={
"width": "100%",
"height": "500px",
"border": "none",
"marginTop": "0px",
"boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"}),
html.P("Deposition of emitted particles when the wind blows from the Northwest for the selected 5 restaurants on Dr Martin Luther King Jr Blvd on February 11th, 2025.", style=text_style),
], style=cell_style),
html.Div([
html.Iframe(
src="/assets/arcgis_embed_4.html",
style={
"width": "100%",
"height": "500px",
"border": "none",
"marginTop": "0px",
"boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"}),
html.P("Deposition of emitted particles when the wind blows from the Southeast for the selected 5 restaurants on Dr Martin Luther King Jr Blvd on March 9th, 2025.", style=text_style),
], style=cell_style),
], style=row_style),
html.Div([
html.Div([
html.Iframe(
src="/assets/arcgis_embed_5.html",
style={
"width": "100%",
"height": "500px",
"border": "none",
"marginTop": "0px",
"boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"}),
html.P("Deposition of emitted particles when the wind blows from the Southwest for the selected 5 restaurants on Dr Martin Luther King Jr Blvd on March 6th, 2025.", style=text_style),
], style=cell_style),
], style=row_style),
], style={
"display": "flex",
"flexDirection": "column",
"alignItems": "center",
"padding": "40px"
}),
], style={"fontFamily": "Inter, Roboto, Arial, sans-serif"})
text_style = {
"color": "#4C4E52",
"marginBottom": "20px",
"textAlign": "center"
}
cell_style = {
"display": "flex",
"flexDirection": "column",
"alignItems": "center",
"margin": "20px"
}
row_style = {
"display": "flex",
"flexDirection": "row",
"justifyContent": "center",
"width": "100%"
}
def air_alerts_page():
return html.Div([
html.Div([
html.H1("School Air Quality Alerts", style={"color": "black"}),
], style={
"backgroundColor": "#ffffff",
"padding": "30px",
"textAlign": "left",
"boxShadow": "0 2px 8px rgba(0, 0, 0, 0.05)"
}),
html.Div([
html.H1("Real Time Air Quality Network - RAAN", style={"color": "#1f2a44"}),
html.H2("What is the Real Time Air Quality Network?", style={"color": "#1f2a44"}),
html.P("Real-Time Air Quality Network, RAAN, is an alert system used by schools throughout the Central Valley. RAAN uses hourly averages of PM 2.5 concentrations to determine the level of air quality, which would then determine if alerts are sent out and what recommendations will be given. This level system is ROAR. When PM 2.5 reaches level 3, an alert will be sent out, and an update will be provided when the PM2.5 drops to level 2 or increases to level 4 or 5 in the following hours. This system collects data from BAM sensors that are managed by SJVAir.", style={"color": "#4C4E52"}),
html.P("ROAR Levels and Guidelines:", style={"color": "#4C4E52"}),
html.Img(src="/assets/roar.png", style={"borderRadius": "8px", "width": "80%", "display": "block", "marginLeft": "auto", "marginRight": "auto", "border": "none", "marginTop": "0px", "boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"}),
html.A("Source: SJVAPCD", href = "https://apps.valleyair.org/myraan/#", target="_blank", className="custom-link", style={"width": "80%", "display": "block", "marginLeft": "auto", "marginRight": "auto", "fontSize": "10px", "color": "#4C4E52"}),
], style={"padding": "40px 40px 0px 40px"}),
html.Div([
html.H2("What is the problem?", style={"color": "#1f2a44"}),
html.P("Relying on the BAM sensors managed by AirNow can be problematic, as these specific BAM sensors are very limited throughout the Central Valley. Therefore, many cities receive alerts based on PM 2.5 concentrations from BAM sensors in other cities. This could result in inaccurate alerts and recommendations being sent out, as air quality could vary between cities.", style={"color": "#4C4E52"}),
html.P("Where are the BAM AirNow sensors?", style={"color": "#4C4E52"}),
html.Img(src="/assets/bam_airnow.png", style={"borderRadius": "8px", "width": "60%", "display": "block", "marginLeft": "auto", "marginRight": "auto", "border": "none", "marginTop": "0px", "boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"}),
html.A("Source: SJVAir", href = "https://www.sjvair.com/", target="_blank", className="custom-link", style={"width": "60%", "display": "block", "marginLeft": "auto", "marginRight": "auto", "fontSize": "10px", "color": "#4C4E52"}),
html.P("This is a screenshot of the SJVAir.com sensor map on April 29, 2025, at 12:50 pm. The map has been filtered to only show BAM air sensors that are managed by AirNow. As shown, there are many places where the nearest AirNow BAM sensor is located very far away. These are the places that are most vulnerable to receiving the wrong alerts.", style={"color": "#4C4E52"}),
], style={"padding": "20px 40px 0px 40px"}),
html.Div([
html.H2("What case studies do we have?", style={"color": "#1f2a44"}),
html.P("In order to understand the accuracy of RAAN and find ways to improve it, we conducted data analysis on PM 2.5 levels reported by different BAM sensors. We focused on specific cities that are vulnerable to receiving the wrong alerts and took the data from the BAM sensor located in this city and compared it to the data collected by the closest BAM sensor managed by AirNow. The nearest AirNow BAM data would be what the RAAN system bases their alerts on, so by comparing these two data sets, we can see how close the trends are to what RAAN is alerting people of versus what is actually happening in the city.", style={"color": "#4C4E52"}),
html.Div([
html.Div([
html.Img(src="/assets/delano_porterville.png", style={
"borderRadius": "8px", "width": "100%", "height": "auto",
"border": "none", "boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"
})
], style={"flex": "1", "paddingRight": "20px"}),
html.Div([
html.H3("Case Study Delano:", style={"color": "#1f2a44"}),
html.P("Delano is a city in the Central Valley. This city does have a BAM sensor, but it is managed by SJVAir. This means that the data collected is not used for RAAN. Instead of the alerts Delano receives from RAAN, they rely on the nearest AirNow BAM sensor, which is in Porterville. Because of the distance between the two cities and their sensors, Delano is receiving the wrong alerts, as the air quality could be different between the two cities.", style={"color": "#4C4E52"}),
html.P("This is a screenshot of the SJVAir.com sensor map on April 28, 2025, at 10:48 am. The map has been filtered to only show BAM air sensors. We can see how far the sensors and cities are from each other.", style={"color": "#4C4E52"}),
html.A("Source: SJVAir", href="https://www.sjvair.com/", target="_blank", className="custom-link", style={"fontSize": "10px", "color": "#4C4E52"}),
], style={"flex": "1"})
], style={"display": "flex", "flexDirection": "row", "marginTop": "20px"})
], style={"padding": "20px 40px 0px 40px"}),
html.Div([
html.Div([
html.Div([
html.P("From the data reported by SJVAir, we created a time series plot. The time series plot shows data from August 01, 2023, to May 01, 2023. It contains both the data from the Delano BAM and the Porterville BAM. The plot also shows the ROAR levels that RAAN uses. As shown in the time series plot, the two datasets at times follow similar trends, but there are many circumstances in which the concentration levels are different, requiring different alerts to be sent out to the two different cities.", style={"color": "#4C4E52"})
], className="responsive-col text"),
html.Div([
dcc.Graph(figure=fig2, className="responsive-graph-2")
], className="responsive-col")
], className="responsive-row"),
], style={"padding": "20px 40px 0px 40px"}),
html.Div([
html.Div([
html.Div([
html.P("To get a better understanding of how often RAAN was sending out inaccurate alerts, we compared how many hours the Delano BAM and Porterville BAM reported reaching a level 3 or above, since this is when RAAN would need to send alerts. The bar plot shows the difference between the amounts reported. As shown, the Delano BAM reported being at a level 3 or above many more times than the Porterville BAM. Since RAAN only bases its alerts on using the Porterville BAM data, this means that Delano didn’t receive all the alerts it should have.", style={"color": "#4C4E52"})
], className="responsive-col text"),
html.Div([
dcc.Graph(figure=fig4, className="responsive-graph-2")
], className="responsive-col")
], className="responsive-row"),
], style={"padding": "20px 40px 0px 40px"}),
html.Div([
html.Div([
html.Div([
html.Img(src="/assets/shafter_bakersfield.png", style={
"borderRadius": "8px", "width": "100%", "height": "auto",
"border": "none", "boxShadow": "0px 4px 6px rgba(0, 0, 0, 0.1)"
})
], style={"flex": "1", "paddingRight": "20px"}),
html.Div([
html.H3("Case Study Shafter:", style={"color": "#1f2a44"}),
html.P("Shafter is also a city located in the Central Valley. This city has multiple BAM sensors, but none of them are managed by AirNow, so they have to rely on an AirNow sensor located in a different city. The closest AirNow BAM is located in Bakersfield. Because of the distance, there might be different air quality detected, which, if ranked at a different level, would mean RAAN sent out the wrong alert.", style={"color": "#4C4E52"}),
html.P("This is a screenshot of the SJVAir.com sensor map on ___ at ___. The map has been filtered to only show BAM air sensors. We can see how far the sensors and cities are from each other.", style={"color": "#4C4E52"}),
html.A("Source: SJVAir", href="https://www.sjvair.com/", target="_blank", className="custom-link", style={"fontSize": "10px", "color": "#4C4E52"}),
], style={"flex": "1"})
], style={"display": "flex", "flexDirection": "row", "marginTop": "20px"})
], style={"padding": "20px 40px 0px 40px"}),
html.Div([
html.Div([
html.Div([
html.P("The plot shows a time series comparison of the Shafter and Bakersfield BAM. It contains data from January 2024 to July 2024. The plot also shows the ROAR levels, as this is what RAAN bases their alert levels on. Once again, we see that the data from the two BAMs does reflect a similar trend, but there are occasions in which Shafter requires a different alert than Bakersfield.", style={"color": "#4C4E52"})
], className="responsive-col text"),
html.Div([
dcc.Graph(figure=fig1, className="responsive-graph-2")
], className="responsive-col")
], className="responsive-row"),
], style={"padding": "20px 40px 0px 40px"}),
html.Div([
html.Div([
html.Div([
html.P("To get a visual understanding of how often RAAN’s alerts were inaccurate, we compared the number of hours the Shafter BAM and Bakersfield BAM reported reaching a level 3 or above, since this is when RAAN sends out alerts. The bar plot shows the visualization of the difference between the two. The bar plot shows that the Bakersfield BAM reported being at a level 3 or above more times than the Shafter BAM. Since RAAN only uses the Bakersfield BAM data to give alerts to Shafter, it means that Shafter received many more alerts than it should have.", style={"color": "#4C4E52"})
], className="responsive-col text"),
html.Div([
dcc.Graph(figure=fig3, className="responsive-graph-2")
], className="responsive-col")
], className="responsive-row"),
], style={"padding": "20px 40px 0px 40px"}),
html.Div([
html.H2("How can RAAN be improved?", style={"color": "#1f2a44"}),
html.P("Based on our case studies, it would be useful for RAAN to be based on more air sensors in order to get more accurate PM2.5 concentrations for different regions. It might be difficult to ask AirNow to add more BAMs to the system due to the high cost of the BAM sensors. However, SJVAir has a network of a variety of air sensors. Some of them are BAMs managed by other organizations. In order to increase the accuracy of RAAN, they could use these other BAM sensors in order to bridge the gap of data gap. With more data available, RAAN could send out more accurate alerts.", style={"color": "#4C4E52"}),
], style={"padding": "20px 40px 40px 40px"}),
], style={"fontFamily": "Inter, Roboto, Arial, sans-serif"})
def community_resources_page():
return html.Div([
html.Div([
html.H1("Community Resources", style={"color": "black"}),
], style={"backgroundColor": "#ffffff", "padding": "30px", "textAlign": "left", "boxShadow": "0 2px 8px rgba(0, 0, 0, 0.05)"}),
html.Div([
html.H2("SJVAir", style={"color": "#1f2a44", "marginBottom": "20px"}),
html.A(
"Visit SJVAir's full website here.",
href="https://www.sjvair.com",
className="custom-link",
target="_blank",
)], style={"padding": "40px", "fontFamily": "Inter, Roboto, Arial, sans-serif"}),
html.Hr(style={'border': 'none', 'height': '0.5px', 'backgroundColor': 'lightgrey', 'margin': '0'}),
html.Div([
html.H2("Community Steering Committees", style={"color": "#1f2a44", "marginBottom": "20px"}),
html.P(
"Under AB617, the San Joaquin Valley Air Pollution Control District has created several steering committees focused on engaging communities in air quality planning and decision-making. They are located in:",
style={"color": "#4C4E52", "marginBottom": "15px"}
),
html.Ul([
html.Li("Arvin/Lamont", style={"color": "#4C4E52", "marginBottom": "5px"}),
html.Li("Stockton", style={"color": "#4C4E52", "marginBottom": "5px"}),
html.Li("South Central Fresno", style={"color": "#4C4E52", "marginBottom": "5px"}),
html.Li("Shafter", style={"color": "#4C4E52", "marginBottom": "15px"}),
]),
html.A(
"Click here to get involved!",
href="https://community.valleyair.org/selected-communities/",
target="_blank",
className="custom-link",
style={"display": "inline-block", "marginTop": "10px"}
),
], style={"padding": "40px", "fontFamily": "Inter, Roboto, Arial, sans-serif"}),
html.Hr(style={'border': 'none', 'height': '0.5px', 'backgroundColor': 'lightgrey', 'margin': '0'}),
html.Div([
html.H2("One Pagers", style={"color": "#1f2a44"}),
html.P("These one-page handouts provide key information and talking points around improving air quality for the Stockton community.", style={"color": "#4C4E52"}),
html.A(
"Click here for Spanish versions!",
href="https://drive.google.com/drive/folders/1REmru7gUNf0Q6vICHikJePfN6X-zvtaH?usp=sharing",
target="_blank",
className="custom-link"
),
html.Ul([
html.Li(html.A("Emissions at the Port of Stockton", href="https://docs.google.com/presentation/d/1fWbJ2TyC_Nr9sWMS-j1pSrZLObrtVKCUvlRWTVVbs4M/present?slide=id.g2730f27e177_0_134", target="_blank", className="custom-link")),
html.Li(html.A("Truck emissions and the Crosstown Freeway", href="https://docs.google.com/presentation/d/1xsBhSnMjZ-cMtmMtv4lzVDWMs8P9cWxRxM_bmP-H93w/present#slide=id.g2730f27e177_0_0", target="_blank", className="custom-link")),
html.Li(html.A("Agricultural burning", href="https://docs.google.com/presentation/d/1RMjV8bJLWAFS10Y1uXCAvucjkNIpNr1tQh3AP564vks/present#slide=id.g2721f55c20f_0_10", target="_blank", className="custom-link")),
html.Li(html.A("Harmful algal blooms", href="https://docs.google.com/presentation/d/1WihVAvJQsAB_u7BjyLPJnLL3wZRKW4vkkR6VEHmy6nk/present?slide=id.g2721f55c20f_0_26", target="_blank", className="custom-link")),
html.Li(html.A("Indoor air quality", href="https://docs.google.com/presentation/d/1BMshfVEz3EYHI1RUi5KvEj8POwBgd2BkCzezO29K4uw/present?slide=id.g2721f55c20f_0_31", target="_blank", className="custom-link")),
], style={"marginTop": "15px", "lineHeight": "1.8"})
], style={"padding": "40px", "fontFamily": "Inter, Roboto, Arial, sans-serif"})
], style={"fontFamily": "Inter, Roboto, Arial, sans-serif"})
# === MAIN APP LAYOUT ===
app.layout = html.Div([
dcc.Location(id='url', refresh=False),
html.Div([
html.Div([
dcc.Link("Home", href="/", className="nav-link"),
dcc.Link("HRRR-Smoke", href="/hrrr-smoke", className="nav-link"),
dcc.Link("HYSPLIT", href="/hysplit", className="nav-link"),
dcc.Link("Air Trends", href="/airtrends", className="nav-link"),
dcc.Link("Restaurant Emissions", href="/restaurants", className="nav-link"),
dcc.Link("Air Alerts", href="/airalerts", className="nav-link"),
dcc.Link("Community Resources", href="/communityresources", className="nav-link"),
], style={
"display": "flex",
"justifyContent": "flex-end",
"gap": "25px",
"padding": "15px 30px",
"backgroundColor": "#1f2a44",
"fontFamily": "Inter, Roboto, sans-serif",
"fontSize": "16px",
"flexWrap": "wrap"
}, className="navbar"),
html.Div(id='page-content')
])
])
# === CALLBACKS ===
@app.callback(
dash.dependencies.Output('page-content', 'children'),
[dash.dependencies.Input('url', 'pathname')]
)
def display_page(pathname):
if pathname == '/':
return home_page()
elif pathname == '/hrrr-smoke':
return hrrr_smoke_page()
elif pathname == '/hysplit':
return hysplit_page()
elif pathname == '/airtrends':
return air_trends_page()
elif pathname == '/restaurants':
return restaurants()
elif pathname == '/airalerts':
return air_alerts_page()
elif pathname == '/communityresources':
return community_resources_page()
else:
return html.Div([
html.H1("404 - Page Not Found", style={"padding": "20px"}),
html.P("The page you are looking for does not exist."),
])
@app.callback(
Output('plotly-graph', 'figure'),
[
Input('date-picker', 'date'),
Input('hour-picker', 'value'),
Input('forecast-picker', 'value')
]
)
def update_plot(selected_date, selected_hour, selected_forecast):
from datetime import datetime
datetime_obj = datetime.strptime(selected_date, '%Y-%m-%d')
datetime_obj = datetime_obj.replace(hour=selected_hour)
datetime_obj = datetime_obj.strftime('%Y-%m-%d %H:%M')
fig = plot_hrrr_smoke_plotly(datetime_obj, selected_forecast)
return fig
def fig_to_base64(fig):
buf = io.BytesIO()
fig.savefig(buf, format="png", bbox_inches="tight")
buf.seek(0)
encoded = base64.b64encode(buf.read()).decode("utf-8")
return f"data:image/png;base64,{encoded}"
@app.callback(
Output('tropomi-map-container', 'children'),
Input('tropomi-date-picker', 'date'),
Input('tropomi-pollutant-dropdown', 'value')
)
def update_tropomi_plot(selected_date, selected_pollutant):
if selected_date is None or selected_pollutant is None:
return html.Div("Please select both a date and a pollutant.")
try:
date_obj = datetime.strptime(selected_date, "%Y-%m-%d")
next_day = date_obj + timedelta(days=1)
next_day_str = next_day.strftime("%Y-%m-%d")
fig = get_pollutant_map(selected_pollutant, selected_date, next_day_str)
img_src = fig_to_base64(fig)
return html.Img(src=img_src, className='responsive-graph')
except Exception as e:
return html.Div(f"Error generating plot: {str(e)}")
# === RUN APP ===
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860, debug=True)