nakas commited on
Commit
ca4110a
·
verified ·
1 Parent(s): 422dcf7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +381 -290
app.py CHANGED
@@ -1,19 +1,24 @@
 
 
1
  import gradio as gr
2
  import pandas as pd
3
  import numpy as np
4
- import re
 
 
 
5
  from playwright.sync_api import sync_playwright
6
  import time
7
  import os
8
  import subprocess
9
  import sys
10
- import matplotlib.pyplot as plt
11
- from matplotlib.gridspec import GridSpec
12
- from windrose import WindroseAxes
13
- from datetime import datetime
14
 
15
- # Install Playwright browsers on startup
16
  def install_playwright_browsers():
 
17
  try:
18
  if not os.path.exists('/home/user/.cache/ms-playwright'):
19
  print("Installing Playwright browsers...")
@@ -27,347 +32,430 @@ def install_playwright_browsers():
27
  except Exception as e:
28
  print(f"Error installing browsers: {e}")
29
 
30
- # Install browsers when the module loads
31
- install_playwright_browsers()
 
 
 
 
 
 
 
 
 
32
 
33
- def scrape_weather_data(site_id, hours=720):
34
- """Scrape weather data from weather.gov timeseries"""
35
- url = f"https://www.weather.gov/wrh/timeseries?site={site_id}&hours={hours}&units=english&chart=on&headers=on&obs=tabular&hourly=false&pview=full&font=12&plot="
36
-
37
  try:
38
- with sync_playwright() as p:
39
- browser = p.chromium.launch(
40
- headless=True,
41
- args=['--no-sandbox', '--disable-dev-shm-usage']
42
- )
43
-
44
- context = browser.new_context(
45
- user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
46
- )
47
-
48
- page = context.new_page()
49
- response = page.goto(url)
50
- print(f"Response status: {response.status}")
51
 
52
- page.wait_for_selector('table', timeout=30000)
53
- time.sleep(5)
54
-
55
- print("Extracting data...")
56
- content = page.evaluate('''() => {
57
- const getTextContent = () => {
58
- const rows = [];
59
- const tables = document.getElementsByTagName('table');
60
- for (const table of tables) {
61
- if (table.textContent.includes('Date/Time')) {
62
- const headerRow = Array.from(table.querySelectorAll('th'))
63
- .map(th => th.textContent.trim());
64
-
65
- const dataRows = Array.from(table.querySelectorAll('tbody tr'))
66
- .map(row => Array.from(row.querySelectorAll('td'))
67
- .map(td => td.textContent.trim()));
68
-
69
- return {headers: headerRow, rows: dataRows};
70
- }
71
- }
72
- return null;
73
- };
74
 
75
- return getTextContent();
76
- }''')
77
-
78
- print(f"Found {len(content['rows'] if content else [])} rows of data")
79
- browser.close()
80
- return content
81
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  except Exception as e:
83
- print(f"Error scraping data: {str(e)}")
84
- raise e
85
 
86
- def parse_date(date_str):
87
- """Parse date string to datetime"""
88
  try:
89
- current_year = datetime.now().year
90
- return pd.to_datetime(f"{date_str}, {current_year}", format="%b %d, %I:%M %p, %Y")
91
- except:
92
- return pd.NaT
93
-
94
- def parse_weather_data(data):
95
- """Parse the weather data into a pandas DataFrame"""
96
- if not data or 'rows' not in data:
97
- raise ValueError("No valid weather data found")
 
98
 
99
- df = pd.DataFrame(data['rows'])
100
-
101
- columns = ['datetime', 'temp', 'dew_point', 'humidity', 'wind_chill',
102
- 'wind_dir', 'wind_speed', 'snow_depth', 'snowfall_3hr',
103
- 'snowfall_6hr', 'snowfall_24hr', 'swe']
104
-
105
- df = df.iloc[:, :12]
106
- df.columns = columns
107
-
108
- numeric_cols = ['temp', 'dew_point', 'humidity', 'wind_chill', 'snow_depth',
109
- 'snowfall_3hr', 'snowfall_6hr', 'snowfall_24hr', 'swe']
110
- for col in numeric_cols:
111
- df[col] = pd.to_numeric(df[col], errors='coerce')
112
-
113
- def parse_wind(x):
114
- if pd.isna(x): return np.nan, np.nan
115
- match = re.search(r'(\d+)G(\d+)', str(x))
116
- if match:
117
- return float(match.group(1)), float(match.group(2))
118
- return np.nan, np.nan
119
-
120
- wind_data = df['wind_speed'].apply(parse_wind)
121
- df['wind_speed'] = wind_data.apply(lambda x: x[0])
122
- df['wind_gust'] = wind_data.apply(lambda x: x[1])
123
-
124
- def parse_direction(direction):
125
- direction_map = {
126
- 'N': 0, 'NNE': 22.5, 'NE': 45, 'ENE': 67.5,
127
- 'E': 90, 'ESE': 112.5, 'SE': 135, 'SSE': 157.5,
128
- 'S': 180, 'SSW': 202.5, 'SW': 225, 'WSW': 247.5,
129
- 'W': 270, 'WNW': 292.5, 'NW': 315, 'NNW': 337.5
130
- }
131
- return direction_map.get(direction, np.nan)
132
-
133
- df['wind_dir_deg'] = df['wind_dir'].apply(parse_direction)
134
-
135
- df['datetime'] = df['datetime'].apply(parse_date)
136
- df['date'] = df['datetime'].dt.date
137
-
138
- return df
139
-
140
- def calculate_total_new_snow(df):
141
- """
142
- Calculate total new snow by:
143
- 1. Using ONLY the 3-hour snowfall amounts
144
- 2. Using 9 AM as the daily reset point
145
- 3. Filtering out obvious anomalies (>9 inches in 3 hours)
146
-
147
- Parameters:
148
- df (pandas.DataFrame): DataFrame with datetime and snowfall_3hr columns
149
-
150
- Returns:
151
- float: Total new snow accumulation
152
- """
153
- # Sort by datetime to ensure correct calculation
154
- df = df.sort_values('datetime')
155
-
156
- # Create a copy of the dataframe with ONLY datetime and 3-hour snowfall
157
- snow_df = df[['datetime', 'snowfall_3hr']].copy()
158
-
159
- # Create a day group that starts at 9 AM instead of midnight
160
- snow_df['day_group'] = snow_df['datetime'].apply(
161
- lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
162
- )
163
-
164
- def process_daily_snow(group):
165
- """Sum up ONLY the 3-hour snowfall amounts for each day period"""
166
- # Sort by time to ensure proper sequence
167
- group = group.sort_values('datetime')
168
 
169
- # Print debugging information
170
- print(f"\nSnowfall amounts for {group['day_group'].iloc[0]}:")
171
- for _, row in group.iterrows():
172
- if pd.notna(row['snowfall_3hr']):
173
- print(f"{row['datetime'].strftime('%Y-%m-%d %H:%M')}: {row['snowfall_3hr']} inches")
 
 
 
 
 
 
 
 
174
 
175
- # Sum only the valid 3-hour amounts, treating NaN as 0
176
- valid_amounts = group['snowfall_3hr'].fillna(0)
177
- daily_total = valid_amounts.sum()
178
 
179
- print(f"Daily total: {daily_total} inches")
180
- return daily_total
181
-
182
- # Calculate daily snow totals
183
- daily_totals = snow_df.groupby('day_group').apply(process_daily_snow)
184
-
185
- return daily_totals.sum()
186
-
187
- def create_daily_snow_plot(df, ax):
188
- """
189
- Create a daily snow plot showing summed 3-hour amounts
190
- """
191
- # Create a copy of the dataframe
192
- snow_df = df[['datetime', 'snowfall_3hr']].copy()
193
-
194
- # Create day groups based on 9 AM reset
195
- snow_df['day_group'] = snow_df['datetime'].apply(
196
- lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
197
- )
198
-
199
- # Calculate daily totals by summing 3-hour amounts
200
- daily_snow = snow_df.groupby('day_group').apply(process_daily_snow).reset_index()
201
- daily_snow.columns = ['date', 'new_snow']
202
-
203
- # Create the bar plot
204
- ax.bar(daily_snow['date'], daily_snow['new_snow'], color='blue')
205
- ax.set_title('Daily New Snow (Sum of 3-hour amounts, 9 AM Reset)', pad=20)
206
- ax.set_xlabel('Date')
207
- ax.set_ylabel('New Snow (inches)')
208
- ax.tick_params(axis='x', rotation=45)
209
- ax.grid(True, axis='y', linestyle='--', alpha=0.7)
210
-
211
- # Add value labels on top of each bar
212
- for i, v in enumerate(daily_snow['new_snow']):
213
- if v > 0: # Only label bars with snow
214
- ax.text(i, v, f'{v:.1f}"',
215
- ha='center', va='bottom')
216
-
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
-
221
- def create_wind_rose(df, ax):
222
- """Create a wind rose plot"""
223
- if not isinstance(ax, WindroseAxes):
224
- ax = WindroseAxes.from_ax(ax=ax)
225
- ax.bar(df['wind_dir_deg'].dropna(), df['wind_speed'].dropna(),
226
- bins=np.arange(0, 40, 5), normed=True, opening=0.8, edgecolor='white')
227
- ax.set_legend(title='Wind Speed (mph)')
228
- ax.set_title('Wind Rose')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
- def create_plots(df):
231
- """Create all weather plots including SWE estimates"""
232
- # Create figure with adjusted height and spacing
233
  fig = plt.figure(figsize=(20, 24))
234
-
235
- # Calculate height ratios for different plots
236
- height_ratios = [1, 1, 1, 1, 1] # Equal height for all plots
237
  gs = GridSpec(5, 1, figure=fig, height_ratios=height_ratios)
238
- gs.update(hspace=0.4) # Increase vertical spacing between plots
239
 
240
  # Temperature plot
241
  ax1 = fig.add_subplot(gs[0])
242
- ax1.plot(df['datetime'], df['temp'], label='Temperature', color='red')
243
- ax1.plot(df['datetime'], df['wind_chill'], label='Wind Chill', color='blue')
244
- ax1.set_title('Temperature and Wind Chill Over Time', pad=20)
245
- ax1.set_xlabel('Date')
246
- ax1.set_ylabel('Temperature (°F)')
247
- ax1.legend()
248
- ax1.grid(True)
 
249
  ax1.tick_params(axis='x', rotation=45)
250
 
251
  # Wind speed plot
252
  ax2 = fig.add_subplot(gs[1])
253
- ax2.plot(df['datetime'], df['wind_speed'], label='Wind Speed', color='blue')
254
- ax2.plot(df['datetime'], df['wind_gust'], label='Wind Gust', color='orange')
255
- ax2.set_title('Wind Speed and Gusts Over Time', pad=20)
256
- ax2.set_xlabel('Date')
257
- ax2.set_ylabel('Wind Speed (mph)')
258
- ax2.legend()
259
- ax2.grid(True)
 
 
 
260
  ax2.tick_params(axis='x', rotation=45)
261
 
262
  # Snow depth plot
263
  ax3 = fig.add_subplot(gs[2])
264
- ax3.plot(df['datetime'], df['snow_depth'], color='blue', label='Snow Depth')
265
- ax3.set_title('Snow Depth Over Time', pad=20)
266
- ax3.set_xlabel('Date')
267
- ax3.set_ylabel('Snow Depth (inches)')
268
- ax3.grid(True)
 
 
 
269
  ax3.tick_params(axis='x', rotation=45)
270
 
271
  # Daily new snow bar plot
272
  ax4 = fig.add_subplot(gs[3])
273
- daily_snow = df.groupby('date')['snowfall_3hr'].sum()
274
- ax4.bar(daily_snow.index, daily_snow.values, color='blue')
275
- ax4.set_title('Daily New Snow', pad=20)
276
- ax4.set_xlabel('Date')
277
- ax4.set_ylabel('New Snow (inches)')
 
 
 
 
 
 
 
 
 
 
 
278
  ax4.tick_params(axis='x', rotation=45)
 
 
 
279
 
280
- # SWE bar plot
281
  ax5 = fig.add_subplot(gs[4])
282
- daily_swe = df.groupby('date')['swe'].mean()
283
- ax5.bar(daily_swe.index, daily_swe.values, color='lightblue')
284
- ax5.set_title('Snow/Water Equivalent', pad=20)
285
- ax5.set_xlabel('Date')
286
- ax5.set_ylabel('SWE (inches)')
 
 
 
 
 
 
 
 
 
287
  ax5.tick_params(axis='x', rotation=45)
 
 
 
288
 
289
- # Adjust layout
290
  plt.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
291
 
292
- # Create separate wind rose figure
293
  fig_rose = plt.figure(figsize=(10, 10))
294
  ax_rose = WindroseAxes.from_ax(fig=fig_rose)
295
- create_wind_rose(df, ax_rose)
 
 
 
296
  fig_rose.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
297
 
298
  return fig, fig_rose
299
 
300
- def analyze_weather_data(site_id, hours):
301
  """Analyze weather data and create visualizations"""
302
  try:
303
- print(f"Scraping data for {site_id}...")
304
- raw_data = scrape_weather_data(site_id, hours)
305
- if not raw_data:
306
- return "Error: Could not retrieve weather data.", None, None
307
-
308
- print("Parsing data...")
309
- df = parse_weather_data(raw_data)
310
-
311
- # Calculate total new snow using the new method
312
- total_new_snow = calculate_total_new_snow(df)
313
- current_swe = df['swe'].iloc[0] # Get most recent SWE measurement
314
-
315
- print("Calculating statistics...")
316
- stats = {
317
- 'Temperature Range': f"{df['temp'].min():.1f}°F to {df['temp'].max():.1f}°F",
318
- 'Average Temperature': f"{df['temp'].mean():.1f}°F",
319
- 'Max Wind Speed': f"{df['wind_speed'].max():.1f} mph",
320
- 'Max Wind Gust': f"{df['wind_gust'].max():.1f} mph",
321
- 'Average Humidity': f"{df['humidity'].mean():.1f}%",
322
- 'Current Snow Depth': f"{df['snow_depth'].iloc[0]:.1f} inches",
323
- 'Total New Snow': f"{total_new_snow:.1f} inches", # Updated to use new calculation
324
- 'Current Snow/Water Equivalent': f"{current_swe:.2f} inches"
325
- }
326
-
327
- html_output = "<div style='font-size: 16px; line-height: 1.5;'>"
328
- html_output += f"<p><strong>Weather Station:</strong> {site_id}</p>"
329
- html_output += f"<p><strong>Data Range:</strong> {df['datetime'].min().strftime('%Y-%m-%d %H:%M')} to {df['datetime'].max().strftime('%Y-%m-%d %H:%M')}</p>"
330
- for key, value in stats.items():
331
- html_output += f"<p><strong>{key}:</strong> {value}</p>"
332
- html_output += "</div>"
333
-
334
- print("Creating plots...")
335
- main_plots, wind_rose = create_plots(df)
336
-
337
- return html_output, main_plots, wind_rose
338
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  except Exception as e:
340
- print(f"Error in analysis: {str(e)}")
341
- return f"Error analyzing data: {str(e)}", None, None
342
 
343
  # Create Gradio interface
344
- with gr.Blocks(title="Weather Station Data Analyzer") as demo:
345
- gr.Markdown("# Weather Station Data Analyzer")
346
  gr.Markdown("""
347
- Enter a weather station ID and number of hours to analyze.
348
- Example station IDs:
349
- - YCTIM (Yellowstone Club - Timber)
350
- - KBZN (Bozeman Airport)
351
- - KSLC (Salt Lake City)
352
  """)
353
 
354
  with gr.Row():
355
- site_id = gr.Textbox(
356
- label="Weather Station ID",
357
- value="YCTIM",
358
- placeholder="Enter station ID (e.g., YCTIM)"
359
- )
360
- hours = gr.Number(
361
- label="Hours of Data",
362
- value=720,
363
  minimum=1,
364
- maximum=1440
 
 
 
 
365
  )
366
 
367
- analyze_btn = gr.Button("Fetch and Analyze Weather Data")
368
 
369
  with gr.Row():
370
- stats_output = gr.HTML(label="Statistics")
371
 
372
  with gr.Row():
373
  weather_plots = gr.Plot(label="Weather Plots")
@@ -375,9 +463,12 @@ with gr.Blocks(title="Weather Station Data Analyzer") as demo:
375
 
376
  analyze_btn.click(
377
  fn=analyze_weather_data,
378
- inputs=[site_id, hours],
379
  outputs=[stats_output, weather_plots, wind_rose]
380
  )
381
 
382
  if __name__ == "__main__":
383
- demo.launch()
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
  import gradio as gr
4
  import pandas as pd
5
  import numpy as np
6
+ import matplotlib.pyplot as plt
7
+ from matplotlib.gridspec import GridSpec
8
+ from windrose import WindroseAxes
9
+ from datetime import datetime, timedelta
10
  from playwright.sync_api import sync_playwright
11
  import time
12
  import os
13
  import subprocess
14
  import sys
15
+ from PIL import Image
16
+ import io
17
+ from zoneinfo import ZoneInfo
18
+ import re
19
 
 
20
  def install_playwright_browsers():
21
+ """Install required Playwright browsers"""
22
  try:
23
  if not os.path.exists('/home/user/.cache/ms-playwright'):
24
  print("Installing Playwright browsers...")
 
32
  except Exception as e:
33
  print(f"Error installing browsers: {e}")
34
 
35
+ def calculate_daily_snow(df):
36
+ """Calculate daily new snow based on maximum value before reset time"""
37
+ df = df.copy()
38
+ # Create a reporting period identifier (4PM to 4PM)
39
+ df['report_date'] = df['datetime'].apply(lambda x:
40
+ (x - timedelta(hours=16)).date() if x.hour >= 16
41
+ else (x - timedelta(days=1, hours=16)).date()
42
+ )
43
+ # Group by reporting period and get the maximum new snow value
44
+ daily_snow = df.groupby('report_date')['new_snow'].max()
45
+ return daily_snow
46
 
47
+ def navigate_to_previous_day(page):
48
+ """Navigate to the previous day using specific selector IDs"""
 
 
49
  try:
50
+ current_values = page.evaluate('''() => {
51
+ const monthSelect = document.getElementById('50');
52
+ const daySelect = document.getElementById('51');
53
+ const yearSelect = document.getElementById('52');
 
 
 
 
 
 
 
 
 
54
 
55
+ return {
56
+ month: parseInt(monthSelect.value),
57
+ day: parseInt(daySelect.value),
58
+ year: parseInt(yearSelect.value)
59
+ };
60
+ }''')
61
+
62
+ current_date = datetime(
63
+ current_values['year'],
64
+ current_values['month'],
65
+ current_values['day']
66
+ )
67
+ previous_date = current_date - timedelta(days=1)
68
+
69
+ print(f"Navigating from {current_date.date()} to {previous_date.date()}")
70
+
71
+ success = page.evaluate('''(prevDate) => {
72
+ try {
73
+ const monthSelect = document.getElementById('50');
74
+ const daySelect = document.getElementById('51');
75
+ const yearSelect = document.getElementById('52');
 
76
 
77
+ yearSelect.value = prevDate.year.toString();
78
+ yearSelect.dispatchEvent(new Event('change', { bubbles: true }));
79
+
80
+ monthSelect.value = prevDate.month.toString();
81
+ monthSelect.dispatchEvent(new Event('change', { bubbles: true }));
82
+
83
+ daySelect.value = prevDate.day.toString();
84
+ daySelect.dispatchEvent(new Event('change', { bubbles: true }));
85
+
86
+ return true;
87
+ } catch (e) {
88
+ console.error('Error setting date:', e);
89
+ return false;
90
+ }
91
+ }''', {
92
+ 'month': previous_date.month,
93
+ 'day': previous_date.day,
94
+ 'year': previous_date.year
95
+ })
96
+
97
+ if success:
98
+ print(f"Successfully navigated to {previous_date.date()}")
99
+
100
+ time.sleep(3)
101
+ return success
102
  except Exception as e:
103
+ print(f"Error navigating to previous day: {str(e)}")
104
+ return False
105
 
106
+ def extract_day_data(page):
107
+ """Extract all data from the current day's table"""
108
  try:
109
+ page.evaluate('''() => {
110
+ const buttons = Array.from(document.querySelectorAll('button'));
111
+ const showAllBtn = buttons.find(b => b.textContent.trim().toLowerCase() === 'show all');
112
+ if (showAllBtn) {
113
+ showAllBtn.click();
114
+ return true;
115
+ }
116
+ return false;
117
+ }''')
118
+ time.sleep(2)
119
 
120
+ current_date = page.evaluate('''() => {
121
+ return {
122
+ month: document.getElementById('50').value,
123
+ day: document.getElementById('51').value,
124
+ year: document.getElementById('52').value
125
+ };
126
+ }''')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ table_data = page.evaluate('''() => {
129
+ const table = document.querySelector('table');
130
+ if (!table) return null;
131
+
132
+ const headers = Array.from(table.querySelectorAll('th'))
133
+ .map(th => th.textContent.trim());
134
+
135
+ const rows = Array.from(table.querySelectorAll('tbody tr'))
136
+ .map(row => Array.from(row.querySelectorAll('td'))
137
+ .map(td => td.textContent.trim()));
138
+
139
+ return { headers, rows };
140
+ }''')
141
 
142
+ return current_date, table_data
 
 
143
 
144
+ except Exception as e:
145
+ print(f"Error extracting day data: {str(e)}")
146
+ return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ def convert_to_dataframe(all_data):
149
+ """Convert collected data to pandas DataFrame format"""
150
+ rows = []
151
+ for data in all_data:
152
+ try:
153
+ date_str = data['date']
154
+ row_data = data['data']
155
+
156
+ if len(row_data) != 9:
157
+ continue
158
+
159
+ # Parse date and time
160
+ parsed_date = datetime.strptime(f"{date_str}", "%m/%d/%Y")
161
+ time_str = row_data[0] if row_data[0] else "12:00AM"
162
+ full_datetime = datetime.strptime(f"{date_str} {time_str}", "%m/%d/%Y %I:%M%p")
163
+
164
+ def clean_numeric(value):
165
+ try:
166
+ if isinstance(value, str):
167
+ cleaned = re.sub(r'[^\d.-]', '', value)
168
+ return float(cleaned) if cleaned else 0.0
169
+ return float(value) if value else 0.0
170
+ except:
171
+ return 0.0
172
 
173
+ row = {
174
+ 'datetime': full_datetime,
175
+ 'temp': clean_numeric(row_data[1]),
176
+ 'new_snow': clean_numeric(row_data[2]),
177
+ 'snow_depth': clean_numeric(row_data[3]),
178
+ 'h2o': clean_numeric(row_data[4]),
179
+ 'humidity': clean_numeric(row_data[5]),
180
+ 'wind_speed': clean_numeric(row_data[6]),
181
+ 'wind_gust': clean_numeric(row_data[7]),
182
+ 'wind_dir': row_data[8],
183
+ 'location': data.get('location', 'alpine')
184
+ }
185
+ rows.append(row)
186
+
187
+ except Exception as e:
188
+ print(f"Error processing row: {str(e)}")
189
+ continue
190
+
191
+ if not rows:
192
+ raise ValueError("No valid data rows to create DataFrame")
193
+
194
+ df = pd.DataFrame(rows)
195
+
196
+ direction_map = {
197
+ 'N': 0, 'NNE': 22.5, 'NE': 45, 'ENE': 67.5,
198
+ 'E': 90, 'ESE': 112.5, 'SE': 135, 'SSE': 157.5,
199
+ 'S': 180, 'SSW': 202.5, 'SW': 225, 'WSW': 247.5,
200
+ 'W': 270, 'WNW': 292.5, 'NW': 315, 'NNW': 337.5
201
+ }
202
+ df['wind_dir_deg'] = df['wind_dir'].map(direction_map)
203
+ df['date'] = df['datetime'].dt.date
204
+ return df.sort_values('datetime')
205
 
206
+ def scrape_location_data(page, location_id, num_days):
207
+ """Scrape data for a specific location"""
208
+ print(f"\nSwitching to location: {location_id}")
209
+ page.evaluate(f'''() => {{
210
+ const locationSelect = document.getElementById('48');
211
+ locationSelect.value = "{location_id}";
212
+ locationSelect.dispatchEvent(new Event('change', {{ bubbles: true }}));
213
+ }}''')
214
+ time.sleep(3) # Wait for location change to take effect
215
+
216
+ all_data = []
217
+ for day in range(num_days):
218
+ print(f"\nProcessing {location_id} - day {day + 1} of {num_days}")
219
+
220
+ # Get current date
221
+ current_date = page.evaluate('''() => {
222
+ return {
223
+ month: document.getElementById('50').value,
224
+ day: document.getElementById('51').value,
225
+ year: document.getElementById('52').value
226
+ };
227
+ }''')
228
+
229
+ date_str = f"{current_date['month']}/{current_date['day']}/{current_date['year']}"
230
+ print(f"Processing date: {date_str}")
231
+
232
+ # Extract data
233
+ _, table_data = extract_day_data(page)
234
+
235
+ if table_data and table_data['rows']:
236
+ rows_found = len(table_data['rows'])
237
+ print(f"Found {rows_found} rows of data")
238
+
239
+ for row in table_data['rows']:
240
+ row_data = {
241
+ 'date': date_str,
242
+ 'headers': table_data['headers'],
243
+ 'data': row,
244
+ 'location': location_id
245
+ }
246
+ all_data.append(row_data)
247
+
248
+ # Navigate to previous day if not the last iteration
249
+ if day < num_days - 1:
250
+ success = navigate_to_previous_day(page)
251
+ if not success:
252
+ print("Failed to navigate to previous day!")
253
+ break
254
+ time.sleep(3)
255
+ else:
256
+ print(f"No data found for {date_str}")
257
+
258
+ return all_data
259
 
260
+ def create_comparison_plots(df_alpine, df_ridge=None):
261
+ """Create weather plots with optional ridge data overlay"""
 
262
  fig = plt.figure(figsize=(20, 24))
263
+ height_ratios = [1, 1, 1, 1, 1]
 
 
264
  gs = GridSpec(5, 1, figure=fig, height_ratios=height_ratios)
265
+ gs.update(hspace=0.4)
266
 
267
  # Temperature plot
268
  ax1 = fig.add_subplot(gs[0])
269
+ ax1.plot(df_alpine['datetime'], df_alpine['temp'], label='Alpine Temperature', color='red', linewidth=2)
270
+ if df_ridge is not None:
271
+ ax1.plot(df_ridge['datetime'], df_ridge['temp'], label='Ridge Temperature', color='darkred', linewidth=2, linestyle='--')
272
+ ax1.set_title('Temperature Over Time', pad=20, fontsize=14)
273
+ ax1.set_xlabel('Date', fontsize=12)
274
+ ax1.set_ylabel('Temperature (°F)', fontsize=12)
275
+ ax1.legend(fontsize=12)
276
+ ax1.grid(True, alpha=0.3)
277
  ax1.tick_params(axis='x', rotation=45)
278
 
279
  # Wind speed plot
280
  ax2 = fig.add_subplot(gs[1])
281
+ ax2.plot(df_alpine['datetime'], df_alpine['wind_speed'], label='Alpine Wind Speed', color='blue', linewidth=2)
282
+ ax2.plot(df_alpine['datetime'], df_alpine['wind_gust'], label='Alpine Wind Gust', color='orange', linewidth=2)
283
+ if df_ridge is not None:
284
+ ax2.plot(df_ridge['datetime'], df_ridge['wind_speed'], label='Ridge Wind Speed', color='darkblue', linewidth=2, linestyle='--')
285
+ ax2.plot(df_ridge['datetime'], df_ridge['wind_gust'], label='Ridge Wind Gust', color='darkorange', linewidth=2, linestyle='--')
286
+ ax2.set_title('Wind Speed and Gusts Over Time', pad=20, fontsize=14)
287
+ ax2.set_xlabel('Date', fontsize=12)
288
+ ax2.set_ylabel('Wind Speed (mph)', fontsize=12)
289
+ ax2.legend(fontsize=12)
290
+ ax2.grid(True, alpha=0.3)
291
  ax2.tick_params(axis='x', rotation=45)
292
 
293
  # Snow depth plot
294
  ax3 = fig.add_subplot(gs[2])
295
+ ax3.plot(df_alpine['datetime'], df_alpine['snow_depth'], color='blue', label='Alpine Snow Depth', linewidth=2)
296
+ if df_ridge is not None:
297
+ ax3.plot(df_ridge['datetime'], df_ridge['snow_depth'], color='darkblue', label='Ridge Snow Depth', linewidth=2, linestyle='--')
298
+ ax3.set_title('Snow Depth Over Time', pad=20, fontsize=14)
299
+ ax3.set_xlabel('Date', fontsize=12)
300
+ ax3.set_ylabel('Snow Depth (inches)', fontsize=12)
301
+ ax3.legend(fontsize=12)
302
+ ax3.grid(True, alpha=0.3)
303
  ax3.tick_params(axis='x', rotation=45)
304
 
305
  # Daily new snow bar plot
306
  ax4 = fig.add_subplot(gs[3])
307
+ daily_snow_alpine = calculate_daily_snow(df_alpine)
308
+ bar_width = 0.35
309
+
310
+ if df_ridge is not None:
311
+ daily_snow_ridge = calculate_daily_snow(df_ridge)
312
+ # Plot bars side by side
313
+ ax4.bar(daily_snow_alpine.index - bar_width/2, daily_snow_alpine.values,
314
+ bar_width, color='blue', alpha=0.7, label='Alpine')
315
+ ax4.bar(daily_snow_ridge.index + bar_width/2, daily_snow_ridge.values,
316
+ bar_width, color='darkblue', alpha=0.7, label='Ridge')
317
+ else:
318
+ ax4.bar(daily_snow_alpine.index, daily_snow_alpine.values, color='blue', alpha=0.7)
319
+
320
+ ax4.set_title('Daily New Snow (4PM to 4PM)', pad=20, fontsize=14)
321
+ ax4.set_xlabel('Date', fontsize=12)
322
+ ax4.set_ylabel('New Snow (inches)', fontsize=12)
323
  ax4.tick_params(axis='x', rotation=45)
324
+ ax4.grid(True, alpha=0.3)
325
+ if df_ridge is not None:
326
+ ax4.legend()
327
 
328
+ # H2O (SWE) plot
329
  ax5 = fig.add_subplot(gs[4])
330
+ daily_swe_alpine = df_alpine.groupby('date')['h2o'].mean()
331
+ if df_ridge is not None:
332
+ daily_swe_ridge = df_ridge.groupby('date')['h2o'].mean()
333
+ ax5.bar(daily_swe_alpine.index - bar_width/2, daily_swe_alpine.values,
334
+ bar_width, color='lightblue', alpha=0.7, label='Alpine')
335
+ ax5.bar(daily_swe_ridge.index + bar_width/2, daily_swe_ridge.values,
336
+ bar_width, color='steelblue', alpha=0.7, label='Ridge')
337
+ else:
338
+ ax5.bar(daily_swe_alpine.index, daily_swe_alpine.values, color='lightblue', alpha=0.7)
339
+
340
+ ax5.set_title('Snow/Water Equivalent', pad=20, fontsize=14
341
+
342
+ ax5.set_xlabel('Date', fontsize=12)
343
+ ax5.set_ylabel('SWE (inches)', fontsize=12)
344
  ax5.tick_params(axis='x', rotation=45)
345
+ ax5.grid(True, alpha=0.3)
346
+ if df_ridge is not None:
347
+ ax5.legend()
348
 
 
349
  plt.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
350
 
351
+ # Create wind rose (alpine only)
352
  fig_rose = plt.figure(figsize=(10, 10))
353
  ax_rose = WindroseAxes.from_ax(fig=fig_rose)
354
+ ax_rose.bar(df_alpine['wind_dir_deg'].dropna(), df_alpine['wind_speed'].dropna(),
355
+ bins=np.arange(0, 40, 5), normed=True, opening=0.8, edgecolor='white')
356
+ ax_rose.set_legend(title='Wind Speed (mph)', fontsize=10)
357
+ ax_rose.set_title('Wind Rose (Alpine)', fontsize=14, pad=20)
358
  fig_rose.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
359
 
360
  return fig, fig_rose
361
 
362
+ def analyze_weather_data(days=3, include_ridge=False):
363
  """Analyze weather data and create visualizations"""
364
  try:
365
+ print("Launching browser...")
366
+ with sync_playwright() as p:
367
+ browser = p.chromium.launch(
368
+ headless=True,
369
+ args=['--no-sandbox', '--disable-dev-shm-usage']
370
+ )
371
+ context = browser.new_context(
372
+ user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
373
+ timezone_id='America/Denver',
374
+ locale='en-US'
375
+ )
376
+
377
+ print("Opening page...")
378
+ page = context.new_page()
379
+ page.goto("https://bridgerbowl.com/weather/history-tables/alpine")
380
+ page.wait_for_load_state('networkidle')
381
+ time.sleep(5)
382
+
383
+ # Scrape Alpine data
384
+ print("\nScraping Alpine data...")
385
+ alpine_data = scrape_location_data(page, "alpine", days)
386
+ df_alpine = convert_to_dataframe(alpine_data)
387
+
388
+ # Scrape Ridge data if requested
389
+ df_ridge = None
390
+ if include_ridge:
391
+ print("\nScraping Ridge data...")
392
+ ridge_data = scrape_location_data(page, "ridge", days)
393
+ df_ridge = convert_to_dataframe(ridge_data)
394
+
395
+ # Create plots and statistics
396
+ print("\nCreating plots...")
397
+ main_plots, wind_rose = create_comparison_plots(df_alpine, df_ridge)
398
+
399
+ # Calculate statistics
400
+ alpine_snow = calculate_daily_snow(df_alpine)
401
+ stats = {
402
+ 'Alpine Temperature Range': f"{df_alpine['temp'].min():.1f}°F to {df_alpine['temp'].max():.1f}°F",
403
+ 'Alpine Average Temperature': f"{df_alpine['temp'].mean():.1f}°F",
404
+ 'Alpine Max Wind Speed': f"{df_alpine['wind_speed'].max():.1f} mph",
405
+ 'Alpine Max Wind Gust': f"{df_alpine['wind_gust'].max():.1f} mph",
406
+ 'Alpine Current Snow Depth': f"{df_alpine['snow_depth'].iloc[0]:.1f} inches",
407
+ 'Alpine Total New Snow': f"{alpine_snow.sum():.1f} inches",
408
+ 'Alpine Current SWE': f"{df_alpine['h2o'].iloc[0]:.2f} inches"
409
+ }
410
+
411
+ if include_ridge and df_ridge is not None:
412
+ ridge_snow = calculate_daily_snow(df_ridge)
413
+ stats.update({
414
+ 'Ridge Temperature Range': f"{df_ridge['temp'].min():.1f}°F to {df_ridge['temp'].max():.1f}°F",
415
+ 'Ridge Average Temperature': f"{df_ridge['temp'].mean():.1f}°F",
416
+ 'Ridge Max Wind Speed': f"{df_ridge['wind_speed'].max():.1f} mph",
417
+ 'Ridge Max Wind Gust': f"{df_ridge['wind_gust'].max():.1f} mph",
418
+ 'Ridge Current Snow Depth': f"{df_ridge['snow_depth'].iloc[0]:.1f} inches",
419
+ 'Ridge Total New Snow': f"{ridge_snow.sum():.1f} inches",
420
+ 'Ridge Current SWE': f"{df_ridge['h2o'].iloc[0]:.2f} inches"
421
+ })
422
+
423
+ # Create HTML report
424
+ html_report = "<h3>Weather Statistics:</h3>"
425
+ for key, value in stats.items():
426
+ html_report += f"<p><strong>{key}:</strong> {value}</p>"
427
+
428
+ browser.close()
429
+ return html_report, main_plots, wind_rose
430
+
431
  except Exception as e:
432
+ print(f"Error during analysis: {str(e)}")
433
+ return f"Error during analysis: {str(e)}", None, None
434
 
435
  # Create Gradio interface
436
+ with gr.Blocks(title="Bridger Bowl Weather Analyzer") as demo:
437
+ gr.Markdown("# Bridger Bowl Weather Analyzer")
438
  gr.Markdown("""
439
+ Analyze weather data from Bridger Bowl's weather stations.
440
+ Specify how many days of historical data to analyze and whether to include Ridge data.
 
 
 
441
  """)
442
 
443
  with gr.Row():
444
+ days_input = gr.Number(
445
+ label="Number of Days to Analyze",
446
+ value=3,
 
 
 
 
 
447
  minimum=1,
448
+ maximum=31
449
+ )
450
+ include_ridge = gr.Checkbox(
451
+ label="Include Ridge Data",
452
+ value=False
453
  )
454
 
455
+ analyze_btn = gr.Button("Collect and Analyze Weather Data")
456
 
457
  with gr.Row():
458
+ stats_output = gr.HTML(label="Statistics and Data Collection Info")
459
 
460
  with gr.Row():
461
  weather_plots = gr.Plot(label="Weather Plots")
 
463
 
464
  analyze_btn.click(
465
  fn=analyze_weather_data,
466
+ inputs=[days_input, include_ridge],
467
  outputs=[stats_output, weather_plots, wind_rose]
468
  )
469
 
470
  if __name__ == "__main__":
471
+ install_playwright_browsers()
472
+ demo.launch()
473
+
474
+