andrewammann's picture
Create app.py
6de6b03 verified
# Run: streamlit run app.py
import streamlit as st
import requests
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime
import warnings
import base64
from io import StringIO
import pytz
from retrying import retry
# Suppress SSL warnings (not recommended for production)
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
# Cache API calls to improve performance
@st.cache_data(ttl=3600)
def get_coordinates(city):
url = f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1&language=en&format=json"
@retry(stop_max_attempt_number=3, wait_fixed=2000)
def fetch():
response = requests.get(url, verify=False, timeout=5)
response.raise_for_status()
return response.json()
try:
data = fetch()
if 'results' in data and data['results']:
return data['results'][0]['latitude'], data['results'][0]['longitude'], data['results'][0]['country'], data['results'][0].get('timezone', 'UTC')
return None, None, None, None
except requests.RequestException as e:
st.error(f"Error fetching coordinates for {city}: {str(e)}")
return None, None, None, None
@st.cache_data(ttl=3600)
def get_weather(lat, lon):
url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_mean,weathercode&current_weather=true&temperature_unit=fahrenheit&timezone=auto"
@retry(stop_max_attempt_number=3, wait_fixed=2000)
def fetch():
response = requests.get(url, verify=False, timeout=5)
response.raise_for_status()
return response.json()
try:
return fetch()
except requests.RequestException as e:
st.error(f"Error fetching weather data: {str(e)}")
return None
# Weather code to icon mapping
weather_icons = {
0: "☀️", 1: "🌤️", 2: "⛅", 3: "☁️", 61: "🌧️", 71: "❄️"
}
# Streamlit configuration
st.set_page_config(page_title="Weather Dashboard", layout="wide", page_icon="🌤️")
# Custom CSS for professional styling
st.markdown("""
<style>
.main {background-color: #f4f6fa;}
.stButton>button {
background-color: #007bff;
color: white;
border-radius: 8px;
padding: 10px 20px;
transition: all 0.3s ease;
}
.stButton>button:hover {
background-color: #0056b3;
transform: scale(1.05);
}
.stTextInput>div>input {
border-radius: 8px;
border: 1px solid #ced4da;
}
.footer {
font-size: 12px;
text-align: center;
margin-top: 30px;
padding: 15px;
background-color: #e9ecef;
border-radius: 8px;
}
.header {
text-align: center;
padding: 20px;
background-color: #007bff;
color: white;
border-radius: 8px;
margin-bottom: 20px;
}
.metric-card {
background-color: #ffffff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
text-align: center;
}
.expander-header {
font-size: 1.5em;
font-weight: bold;
}
</style>
""", unsafe_allow_html=True)
# Header with logo placeholder
st.markdown("""
<div class='header'>
<h1>🌍 Global Weather Dashboard</h1>
<p>Powered by Open-Meteo API</p>
<!-- Replace with your logo -->
<img src="https://via.placeholder.com/100x50.png?text=Logo" style="margin-top: 10px;">
</div>
""", unsafe_allow_html=True)
# Sidebar for controls
with st.sidebar:
st.header("Dashboard Settings")
locations_input = st.text_input("Enter city names (comma-separated):", "New York, London, Tokyo", help="E.g., New York, London, Tokyo")
predefined_cities = ["New York", "London", "Tokyo", "Sydney", "Paris", "Dubai", "Singapore"]
selected_city = st.selectbox("Or select a city:", [""] + predefined_cities, help="Choose a city for quick access")
chart_type = st.radio("Chart Type:", ["Separate Charts", "Combined Chart"], help="Choose how to display weather charts")
if st.button("Fetch Weather", key="fetch_button"):
st.session_state['fetch'] = True
with st.expander("Security Info"):
st.warning("⚠️ Using verify=False for SSL. This is insecure for production. Ensure valid SSL certificates for secure deployment.")
# Main content
if 'fetch' in st.session_state and st.session_state['fetch']:
cities = [selected_city] if selected_city else [city.strip() for city in locations_input.split(',') if city.strip()]
cities = list(dict.fromkeys(cities)) # Remove duplicates
if not cities:
st.warning("Please enter or select at least one city.")
else:
with st.spinner("Fetching weather data..."):
# Collect coordinates for map
coordinates = []
for city in cities:
lat, lon, country, timezone = get_coordinates(city)
if lat and lon:
weather_data = get_weather(lat, lon)
if weather_data and 'current_weather' in weather_data:
temp = weather_data['current_weather'].get('temperature', 0)
coordinates.append((city, lat, lon, country, timezone, temp))
# Render Plotly map
st.markdown("### City Locations")
if coordinates:
df_map = pd.DataFrame(coordinates, columns=['City', 'Latitude', 'Longitude', 'Country', 'Timezone', 'Current Temp (°F)'])
fig_map = px.scatter_geo(
df_map,
lat='Latitude',
lon='Longitude',
hover_name='City',
hover_data=['Country', 'Current Temp (°F)'],
title="City Locations (Colored by Current Temperature)",
projection="natural earth",
color='Current Temp (°F)',
color_continuous_scale='RdBu_r',
range_color=[df_map['Current Temp (°F)'].min(), df_map['Current Temp (°F)'].max()]
)
fig_map.update_layout(
showlegend=True,
geo=dict(
showland=True,
landcolor="#e9ecef",
showcountries=True,
countrycolor="#cccccc",
bgcolor="#f4f6fa"
),
font=dict(size=12),
margin=dict(l=20, r=20, t=50, b=20)
)
fig_map.update_traces(marker=dict(size=12, line=dict(width=1, color='DarkSlateGrey')))
st.plotly_chart(fig_map, use_container_width=True)
else:
st.warning("No valid coordinates found for the provided cities.")
# Weather data for each city
for city in cities:
with st.expander(f"🌆 Weather for {city}", expanded=True):
lat, lon, country, timezone = get_coordinates(city)
if lat and lon:
st.write(f"📍 {city}, {country} (Lat: {lat:.2f}, Lon: {lon:.2f})")
local_time = datetime.now(pytz.timezone(timezone)).strftime("%Y-%m-%d %H:%M:%S %Z")
st.write(f"🕒 Local Time: {local_time}")
weather_data = get_weather(lat, lon)
if weather_data:
# Current Weather
current = weather_data.get('current_weather', {})
st.markdown("#### Current Weather", unsafe_allow_html=True)
col1, col2, col3 = st.columns([1, 1, 1])
with col1:
st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
st.metric("Temperature", f"{current.get('temperature', 'N/A')} °F")
st.markdown("</div>", unsafe_allow_html=True)
with col2:
st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
st.metric("Wind Speed", f"{current.get('windspeed', 'N/A')} km/h")
st.markdown("</div>", unsafe_allow_html=True)
with col3:
st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
weather_code = current.get('weathercode', 0)
st.metric("Condition", f"{weather_icons.get(weather_code, '🌫️')} {weather_code}")
st.markdown("</div>", unsafe_allow_html=True)
# Daily Forecast Table
daily = weather_data.get('daily', {})
if daily:
df = pd.DataFrame({
'Date': pd.to_datetime(daily['time']),
'Max Temp (°F)': daily['temperature_2m_max'],
'Min Temp (°F)': daily['temperature_2m_min'],
'Precipitation Prob (%)': [prob * 100 if prob is not None else 0 for prob in daily['precipitation_probability_mean']],
'Condition': [weather_icons.get(code, '🌫️') for code in daily['weathercode']]
})
st.markdown("#### 7-Day Forecast")
st.dataframe(df.style.format({
'Max Temp (°F)': '{:.1f}',
'Min Temp (°F)': '{:.1f}',
'Precipitation Prob (%)': '{:.0f}',
'Date': '{:%Y-%m-%d}'
}).background_gradient(subset=['Max Temp (°F)'], cmap='Reds'))
# Summary Statistics
st.markdown("#### Summary Statistics")
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
st.metric("Avg Max Temp", f"{df['Max Temp (°F)'].mean():.1f} °F")
st.markdown("</div>", unsafe_allow_html=True)
with col2:
st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
st.metric("Avg Min Temp", f"{df['Min Temp (°F)'].mean():.1f} °F")
st.markdown("</div>", unsafe_allow_html=True)
with col3:
st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
st.metric("Avg Precipitation Prob", f"{df['Precipitation Prob (%)'].mean():.0f}%")
st.markdown("</div>", unsafe_allow_html=True)
# Download CSV
csv = df.to_csv(index=False)
b64 = base64.b64encode(csv.encode()).decode()
href = f'<a href="data:file/csv;base64,{b64}" download="{city}_forecast.csv">Download Forecast as CSV</a>'
st.markdown(href, unsafe_allow_html=True)
# Plotly Charts
if chart_type == "Separate Charts":
# Temperature Line Chart with Shaded Area
st.markdown("#### Temperature Forecast")
fig_temp = go.Figure()
fig_temp.add_trace(go.Scatter(
x=df['Date'], y=df['Max Temp (°F)'],
name='Max Temp (°F)', line=dict(color='#ff4d4d'),
hovertemplate='Max Temp: %{y:.1f}°F<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
customdata=df['Condition']
))
fig_temp.add_trace(go.Scatter(
x=df['Date'], y=df['Min Temp (°F)'],
name='Min Temp (°F)', line=dict(color='#4d79ff'),
hovertemplate='Min Temp: %{y:.1f}°F<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
customdata=df['Condition'],
fill='tonexty', fillcolor='rgba(77, 121, 255, 0.1)'
))
max_temp_idx = df['Max Temp (°F)'].idxmax()
fig_temp.add_annotation(
x=df['Date'][max_temp_idx], y=df['Max Temp (°F)'][max_temp_idx],
text=f"High: {df['Max Temp (°F)'][max_temp_idx]:.1f}°F",
showarrow=True, arrowhead=2, ax=20, ay=-30
)
fig_temp.update_layout(
showlegend=True,
template='plotly_white',
hovermode='x unified',
xaxis_title="Date",
yaxis_title="Temperature (°F)",
font=dict(size=12),
margin=dict(l=20, r=20, t=50, b=20)
)
st.plotly_chart(fig_temp, use_container_width=True)
# Precipitation Bar Chart
st.markdown("#### Precipitation Probability")
fig_precip = go.Figure(data=[
go.Bar(
x=df['Date'],
y=df['Precipitation Prob (%)'],
marker_color='#1e90ff',
hovertemplate='Precipitation: %{y:.0f}%<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
customdata=df['Condition']
)
])
max_precip_idx = df['Precipitation Prob (%)'].idxmax()
fig_precip.add_annotation(
x=df['Date'][max_precip_idx], y=df['Precipitation Prob (%)'][max_precip_idx],
text=f"Max: {df['Precipitation Prob (%)'][max_precip_idx]:.0f}%",
showarrow=True, arrowhead=2, ax=20, ay=-30
)
fig_precip.update_layout(
title=f"Precipitation Probability for {city}",
xaxis_title="Date",
yaxis_title="Probability (%)",
template='plotly_white',
font=dict(size=12),
margin=dict(l=20, r=20, t=50, b=20)
)
st.plotly_chart(fig_precip, use_container_width=True)
else:
# Combined Chart
st.markdown("#### Combined Temperature and Precipitation Forecast")
fig_combined = go.Figure()
fig_combined.add_trace(go.Scatter(
x=df['Date'], y=df['Max Temp (°F)'],
name='Max Temp (°F)', line=dict(color='#ff4d4d'),
hovertemplate='Max Temp: %{y:.1f}°F<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
customdata=df['Condition']
))
fig_combined.add_trace(go.Scatter(
x=df['Date'], y=df['Min Temp (°F)'],
name='Min Temp (°F)', line=dict(color='#4d79ff'),
hovertemplate='Min Temp: %{y:.1f}°F<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
customdata=df['Condition'],
fill='tonexty', fillcolor='rgba(77, 121, 255, 0.1)'
))
fig_combined.add_trace(go.Bar(
x=df['Date'], y=df['Precipitation Prob (%)'],
name='Precipitation (%)', yaxis='y2', marker_color='#1e90ff',
hovertemplate='Precipitation: %{y:.0f}%<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
customdata=df['Condition'],
opacity=0.4
))
max_temp_idx = df['Max Temp (°F)'].idxmax()
fig_combined.add_annotation(
x=df['Date'][max_temp_idx], y=df['Max Temp (°F)'][max_temp_idx],
text=f"High: {df['Max Temp (°F)'][max_temp_idx]:.1f}°F",
showarrow=True, arrowhead=2, ax=20, ay=-30
)
fig_combined.update_layout(
title=f"Combined Forecast for {city}",
xaxis_title="Date",
yaxis=dict(title="Temperature (°F)", titlefont=dict(color="#ff4d4d"), tickfont=dict(color="#ff4d4d")),
yaxis2=dict(title="Precipitation (%)", titlefont=dict(color="#1e90ff"), tickfont=dict(color="#1e90ff"),
overlaying='y', side='right'),
template='plotly_white',
hovermode='x unified',
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5),
font=dict(size=12),
margin=dict(l=20, r=20, t=50, b=20)
)
st.plotly_chart(fig_combined, use_container_width=True)
else:
st.error(f"Failed to fetch weather data for {city}.")
else:
st.error(f"Could not find coordinates for {city}.")
# Footer
st.markdown("""
<div class='footer'>
<p>Weather data by <a href="https://open-meteo.com" target="_blank">Open-Meteo.com</a> under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0</a> |
For non-commercial use only |
<a href="https://github.com/open-meteo/open-meteo" target="_blank">Source Code</a> |
Contact: <a href="mailto:info@open-meteo.com">info@open-meteo.com</a></p>
</div>
""", unsafe_allow_html=True)