andrewammann commited on
Commit
051f91c
·
verified ·
1 Parent(s): d8edd54

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +367 -0
app.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Run: streamlit run app.py
2
+ import streamlit as st
3
+ import requests
4
+ import pandas as pd
5
+ import plotly.express as px
6
+ import plotly.graph_objects as go
7
+ from datetime import datetime
8
+ import warnings
9
+ import base64
10
+ from io import StringIO
11
+ import pytz
12
+ from retrying import retry
13
+
14
+ # Suppress SSL warnings (not recommended for production)
15
+ warnings.filterwarnings('ignore', message='Unverified HTTPS request')
16
+
17
+ # Cache API calls to improve performance
18
+ @st.cache_data(ttl=3600)
19
+ def get_coordinates(city):
20
+ url = f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1&language=en&format=json"
21
+ @retry(stop_max_attempt_number=3, wait_fixed=2000)
22
+ def fetch():
23
+ response = requests.get(url, verify=False, timeout=5)
24
+ response.raise_for_status()
25
+ return response.json()
26
+ try:
27
+ data = fetch()
28
+ if 'results' in data and data['results']:
29
+ return data['results'][0]['latitude'], data['results'][0]['longitude'], data['results'][0]['country'], data['results'][0].get('timezone', 'UTC')
30
+ return None, None, None, None
31
+ except requests.RequestException as e:
32
+ st.error(f"Error fetching coordinates for {city}: {str(e)}")
33
+ return None, None, None, None
34
+
35
+ @st.cache_data(ttl=3600)
36
+ def get_weather(lat, lon):
37
+ 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"
38
+ @retry(stop_max_attempt_number=3, wait_fixed=2000)
39
+ def fetch():
40
+ response = requests.get(url, verify=False, timeout=5)
41
+ response.raise_for_status()
42
+ return response.json()
43
+ try:
44
+ return fetch()
45
+ except requests.RequestException as e:
46
+ st.error(f"Error fetching weather data: {str(e)}")
47
+ return None
48
+
49
+ # Weather code to icon mapping
50
+ weather_icons = {
51
+ 0: "☀️", 1: "🌤️", 2: "⛅", 3: "☁️", 61: "🌧️", 71: "❄️"
52
+ }
53
+
54
+ # Streamlit configuration
55
+ st.set_page_config(page_title="Weather Dashboard", layout="wide", page_icon="🌤️")
56
+
57
+ # Custom CSS for professional styling
58
+ st.markdown("""
59
+ <style>
60
+ .main {background-color: #f4f6fa;}
61
+ .stButton>button {
62
+ background-color: #007bff;
63
+ color: white;
64
+ border-radius: 8px;
65
+ padding: 10px 20px;
66
+ transition: all 0.3s ease;
67
+ }
68
+ .stButton>button:hover {
69
+ background-color: #0056b3;
70
+ transform: scale(1.05);
71
+ }
72
+ .stTextInput>div>input {
73
+ border-radius: 8px;
74
+ border: 1px solid #ced4da;
75
+ }
76
+ .footer {
77
+ font-size: 12px;
78
+ text-align: center;
79
+ margin-top: 30px;
80
+ padding: 15px;
81
+ background-color: #e9ecef;
82
+ border-radius: 8px;
83
+ }
84
+ .header {
85
+ text-align: center;
86
+ padding: 20px;
87
+ background-color: #007bff;
88
+ color: white;
89
+ border-radius: 8px;
90
+ margin-bottom: 20px;
91
+ }
92
+ .metric-card {
93
+ background-color: #ffffff;
94
+ padding: 15px;
95
+ border-radius: 8px;
96
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
97
+ text-align: center;
98
+ }
99
+ .expander-header {
100
+ font-size: 1.5em;
101
+ font-weight: bold;
102
+ }
103
+ </style>
104
+ """, unsafe_allow_html=True)
105
+
106
+ # Header with logo placeholder
107
+ st.markdown("""
108
+ <div class='header'>
109
+ <h1>🌍 Global Weather Dashboard</h1>
110
+ <p>Powered by Open-Meteo API</p>
111
+ <!-- Replace with your logo -->
112
+ <img src="https://via.placeholder.com/100x50.png?text=Logo" style="margin-top: 10px;">
113
+ </div>
114
+ """, unsafe_allow_html=True)
115
+
116
+ # Sidebar for controls
117
+ with st.sidebar:
118
+ st.header("Dashboard Settings")
119
+ locations_input = st.text_input("Enter city names (comma-separated):", "New York, London, Tokyo", help="E.g., New York, London, Tokyo")
120
+ predefined_cities = ["New York", "London", "Tokyo", "Sydney", "Paris", "Dubai", "Singapore"]
121
+ selected_city = st.selectbox("Or select a city:", [""] + predefined_cities, help="Choose a city for quick access")
122
+ chart_type = st.radio("Chart Type:", ["Separate Charts", "Combined Chart"], help="Choose how to display weather charts")
123
+ if st.button("Fetch Weather", key="fetch_button"):
124
+ st.session_state['fetch'] = True
125
+ with st.expander("Security Info"):
126
+ st.warning("⚠️ Using verify=False for SSL. This is insecure for production. Ensure valid SSL certificates for secure deployment.")
127
+
128
+ # Main content
129
+ if 'fetch' in st.session_state and st.session_state['fetch']:
130
+ cities = [selected_city] if selected_city else [city.strip() for city in locations_input.split(',') if city.strip()]
131
+ cities = list(dict.fromkeys(cities)) # Remove duplicates
132
+
133
+ if not cities:
134
+ st.warning("Please enter or select at least one city.")
135
+ else:
136
+ with st.spinner("Fetching weather data..."):
137
+ # Collect coordinates for map
138
+ coordinates = []
139
+ for city in cities:
140
+ lat, lon, country, timezone = get_coordinates(city)
141
+ if lat and lon:
142
+ weather_data = get_weather(lat, lon)
143
+ if weather_data and 'current_weather' in weather_data:
144
+ temp = weather_data['current_weather'].get('temperature', 0)
145
+ coordinates.append((city, lat, lon, country, timezone, temp))
146
+
147
+ # Render Plotly map
148
+ st.markdown("### City Locations")
149
+ if coordinates:
150
+ df_map = pd.DataFrame(coordinates, columns=['City', 'Latitude', 'Longitude', 'Country', 'Timezone', 'Current Temp (°F)'])
151
+ fig_map = px.scatter_geo(
152
+ df_map,
153
+ lat='Latitude',
154
+ lon='Longitude',
155
+ hover_name='City',
156
+ hover_data=['Country', 'Current Temp (°F)'],
157
+ title="City Locations (Colored by Current Temperature)",
158
+ projection="natural earth",
159
+ color='Current Temp (°F)',
160
+ color_continuous_scale='RdBu_r',
161
+ range_color=[df_map['Current Temp (°F)'].min(), df_map['Current Temp (°F)'].max()]
162
+ )
163
+ fig_map.update_layout(
164
+ showlegend=True,
165
+ geo=dict(
166
+ showland=True,
167
+ landcolor="#e9ecef",
168
+ showcountries=True,
169
+ countrycolor="#cccccc",
170
+ bgcolor="#f4f6fa"
171
+ ),
172
+ font=dict(size=12),
173
+ margin=dict(l=20, r=20, t=50, b=20)
174
+ )
175
+ fig_map.update_traces(marker=dict(size=12, line=dict(width=1, color='DarkSlateGrey')))
176
+ st.plotly_chart(fig_map, use_container_width=True)
177
+ else:
178
+ st.warning("No valid coordinates found for the provided cities.")
179
+
180
+ # Weather data for each city
181
+ for city in cities:
182
+ with st.expander(f"🌆 Weather for {city}", expanded=True):
183
+ lat, lon, country, timezone = get_coordinates(city)
184
+ if lat and lon:
185
+ st.write(f"📍 {city}, {country} (Lat: {lat:.2f}, Lon: {lon:.2f})")
186
+ local_time = datetime.now(pytz.timezone(timezone)).strftime("%Y-%m-%d %H:%M:%S %Z")
187
+ st.write(f"🕒 Local Time: {local_time}")
188
+
189
+ weather_data = get_weather(lat, lon)
190
+ if weather_data:
191
+ # Current Weather
192
+ current = weather_data.get('current_weather', {})
193
+ st.markdown("#### Current Weather", unsafe_allow_html=True)
194
+ col1, col2, col3 = st.columns([1, 1, 1])
195
+ with col1:
196
+ st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
197
+ st.metric("Temperature", f"{current.get('temperature', 'N/A')} °F")
198
+ st.markdown("</div>", unsafe_allow_html=True)
199
+ with col2:
200
+ st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
201
+ st.metric("Wind Speed", f"{current.get('windspeed', 'N/A')} km/h")
202
+ st.markdown("</div>", unsafe_allow_html=True)
203
+ with col3:
204
+ st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
205
+ weather_code = current.get('weathercode', 0)
206
+ st.metric("Condition", f"{weather_icons.get(weather_code, '🌫️')} {weather_code}")
207
+ st.markdown("</div>", unsafe_allow_html=True)
208
+
209
+ # Daily Forecast Table
210
+ daily = weather_data.get('daily', {})
211
+ if daily:
212
+ df = pd.DataFrame({
213
+ 'Date': pd.to_datetime(daily['time']),
214
+ 'Max Temp (°F)': daily['temperature_2m_max'],
215
+ 'Min Temp (°F)': daily['temperature_2m_min'],
216
+ 'Precipitation Prob (%)': [prob * 100 if prob is not None else 0 for prob in daily['precipitation_probability_mean']],
217
+ 'Condition': [weather_icons.get(code, '🌫️') for code in daily['weathercode']]
218
+ })
219
+ st.markdown("#### 7-Day Forecast")
220
+ st.dataframe(df.style.format({
221
+ 'Max Temp (°F)': '{:.1f}',
222
+ 'Min Temp (°F)': '{:.1f}',
223
+ 'Precipitation Prob (%)': '{:.0f}',
224
+ 'Date': '{:%Y-%m-%d}'
225
+ }).background_gradient(subset=['Max Temp (°F)'], cmap='Reds'))
226
+
227
+ # Summary Statistics
228
+ st.markdown("#### Summary Statistics")
229
+ col1, col2, col3 = st.columns(3)
230
+ with col1:
231
+ st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
232
+ st.metric("Avg Max Temp", f"{df['Max Temp (°F)'].mean():.1f} °F")
233
+ st.markdown("</div>", unsafe_allow_html=True)
234
+ with col2:
235
+ st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
236
+ st.metric("Avg Min Temp", f"{df['Min Temp (°F)'].mean():.1f} °F")
237
+ st.markdown("</div>", unsafe_allow_html=True)
238
+ with col3:
239
+ st.markdown("<div class='metric-card'>", unsafe_allow_html=True)
240
+ st.metric("Avg Precipitation Prob", f"{df['Precipitation Prob (%)'].mean():.0f}%")
241
+ st.markdown("</div>", unsafe_allow_html=True)
242
+
243
+ # Download CSV
244
+ csv = df.to_csv(index=False)
245
+ b64 = base64.b64encode(csv.encode()).decode()
246
+ href = f'<a href="data:file/csv;base64,{b64}" download="{city}_forecast.csv">Download Forecast as CSV</a>'
247
+ st.markdown(href, unsafe_allow_html=True)
248
+
249
+ # Plotly Charts
250
+ if chart_type == "Separate Charts":
251
+ # Temperature Line Chart with Shaded Area
252
+ st.markdown("#### Temperature Forecast")
253
+ fig_temp = go.Figure()
254
+ fig_temp.add_trace(go.Scatter(
255
+ x=df['Date'], y=df['Max Temp (°F)'],
256
+ name='Max Temp (°F)', line=dict(color='#ff4d4d'),
257
+ hovertemplate='Max Temp: %{y:.1f}°F<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
258
+ customdata=df['Condition']
259
+ ))
260
+ fig_temp.add_trace(go.Scatter(
261
+ x=df['Date'], y=df['Min Temp (°F)'],
262
+ name='Min Temp (°F)', line=dict(color='#4d79ff'),
263
+ hovertemplate='Min Temp: %{y:.1f}°F<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
264
+ customdata=df['Condition'],
265
+ fill='tonexty', fillcolor='rgba(77, 121, 255, 0.1)'
266
+ ))
267
+ max_temp_idx = df['Max Temp (°F)'].idxmax()
268
+ fig_temp.add_annotation(
269
+ x=df['Date'][max_temp_idx], y=df['Max Temp (°F)'][max_temp_idx],
270
+ text=f"High: {df['Max Temp (°F)'][max_temp_idx]:.1f}°F",
271
+ showarrow=True, arrowhead=2, ax=20, ay=-30
272
+ )
273
+ fig_temp.update_layout(
274
+ showlegend=True,
275
+ template='plotly_white',
276
+ hovermode='x unified',
277
+ xaxis_title="Date",
278
+ yaxis_title="Temperature (°F)",
279
+ font=dict(size=12),
280
+ margin=dict(l=20, r=20, t=50, b=20)
281
+ )
282
+ st.plotly_chart(fig_temp, use_container_width=True)
283
+
284
+ # Precipitation Bar Chart
285
+ st.markdown("#### Precipitation Probability")
286
+ fig_precip = go.Figure(data=[
287
+ go.Bar(
288
+ x=df['Date'],
289
+ y=df['Precipitation Prob (%)'],
290
+ marker_color='#1e90ff',
291
+ hovertemplate='Precipitation: %{y:.0f}%<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
292
+ customdata=df['Condition']
293
+ )
294
+ ])
295
+ max_precip_idx = df['Precipitation Prob (%)'].idxmax()
296
+ fig_precip.add_annotation(
297
+ x=df['Date'][max_precip_idx], y=df['Precipitation Prob (%)'][max_precip_idx],
298
+ text=f"Max: {df['Precipitation Prob (%)'][max_precip_idx]:.0f}%",
299
+ showarrow=True, arrowhead=2, ax=20, ay=-30
300
+ )
301
+ fig_precip.update_layout(
302
+ title=f"Precipitation Probability for {city}",
303
+ xaxis_title="Date",
304
+ yaxis_title="Probability (%)",
305
+ template='plotly_white',
306
+ font=dict(size=12),
307
+ margin=dict(l=20, r=20, t=50, b=20)
308
+ )
309
+ st.plotly_chart(fig_precip, use_container_width=True)
310
+
311
+ else:
312
+ # Combined Chart
313
+ st.markdown("#### Combined Temperature and Precipitation Forecast")
314
+ fig_combined = go.Figure()
315
+ fig_combined.add_trace(go.Scatter(
316
+ x=df['Date'], y=df['Max Temp (°F)'],
317
+ name='Max Temp (°F)', line=dict(color='#ff4d4d'),
318
+ hovertemplate='Max Temp: %{y:.1f}°F<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
319
+ customdata=df['Condition']
320
+ ))
321
+ fig_combined.add_trace(go.Scatter(
322
+ x=df['Date'], y=df['Min Temp (°F)'],
323
+ name='Min Temp (°F)', line=dict(color='#4d79ff'),
324
+ hovertemplate='Min Temp: %{y:.1f}°F<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
325
+ customdata=df['Condition'],
326
+ fill='tonexty', fillcolor='rgba(77, 121, 255, 0.1)'
327
+ ))
328
+ fig_combined.add_trace(go.Bar(
329
+ x=df['Date'], y=df['Precipitation Prob (%)'],
330
+ name='Precipitation (%)', yaxis='y2', marker_color='#1e90ff',
331
+ hovertemplate='Precipitation: %{y:.0f}%<br>%{x|%Y-%m-%d}<br>Condition: %{customdata}',
332
+ customdata=df['Condition'],
333
+ opacity=0.4
334
+ ))
335
+ max_temp_idx = df['Max Temp (°F)'].idxmax()
336
+ fig_combined.add_annotation(
337
+ x=df['Date'][max_temp_idx], y=df['Max Temp (°F)'][max_temp_idx],
338
+ text=f"High: {df['Max Temp (°F)'][max_temp_idx]:.1f}°F",
339
+ showarrow=True, arrowhead=2, ax=20, ay=-30
340
+ )
341
+ fig_combined.update_layout(
342
+ title=f"Combined Forecast for {city}",
343
+ xaxis_title="Date",
344
+ yaxis=dict(title="Temperature (°F)", titlefont=dict(color="#ff4d4d"), tickfont=dict(color="#ff4d4d")),
345
+ yaxis2=dict(title="Precipitation (%)", titlefont=dict(color="#1e90ff"), tickfont=dict(color="#1e90ff"),
346
+ overlaying='y', side='right'),
347
+ template='plotly_white',
348
+ hovermode='x unified',
349
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5),
350
+ font=dict(size=12),
351
+ margin=dict(l=20, r=20, t=50, b=20)
352
+ )
353
+ st.plotly_chart(fig_combined, use_container_width=True)
354
+ else:
355
+ st.error(f"Failed to fetch weather data for {city}.")
356
+ else:
357
+ st.error(f"Could not find coordinates for {city}.")
358
+
359
+ # Footer
360
+ st.markdown("""
361
+ <div class='footer'>
362
+ <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> |
363
+ For non-commercial use only |
364
+ <a href="https://github.com/open-meteo/open-meteo" target="_blank">Source Code</a> |
365
+ Contact: <a href="mailto:info@open-meteo.com">info@open-meteo.com</a></p>
366
+ </div>
367
+ """, unsafe_allow_html=True)