github-actions[bot] commited on
Commit
2dc4d04
·
1 Parent(s): e9832fd

Deploy from GitHub Actions

Browse files
Files changed (3) hide show
  1. plots/map.py +330 -26
  2. ui/pages/home.py +0 -1
  3. ui/pages/seasonal_maps.py +68 -11
plots/map.py CHANGED
@@ -8,6 +8,7 @@ import numpy as np
8
  import pandas as pd
9
  import streamlit as st
10
  from matplotlib.colors import LinearSegmentedColormap
 
11
  from osgeo import gdal
12
 
13
  from utils.data_loading import timer
@@ -213,8 +214,320 @@ def plot_seasonal_salinity_for_bays(
213
 
214
 
215
  @timer(include_params=True)
216
- def generate_seasonal_plot(data, year, shapefile_path):
217
- """Generate the seasonal trends plot"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  # Add debugging information
219
  wbids = gpd.read_file(shapefile_path)
220
 
@@ -231,28 +544,19 @@ def generate_seasonal_plot(data, year, shapefile_path):
231
  # Pre-transform to Web Mercator (EPSG:3857) here to avoid issues in plotting function
232
  wbids = wbids.to_crs(epsg=3857)
233
 
234
- if st.session_state.get("DEBUG", False):
235
- st.write("Debug Info:")
236
- st.write(
237
- {
238
- "Shapefile CRS": wbids.crs,
239
- "Input Data CRS": data.crs
240
- if isinstance(data, gpd.GeoDataFrame)
241
- else "Not a GeoDataFrame",
242
- "GDAL Version": gdal.VersionInfo()
243
- if "osgeo.gdal" in sys.modules
244
- else "Not available",
245
- "GeoPandas Version": gpd.__version__,
246
- "Python Version": sys.version,
247
- "File exists": Path(shapefile_path).exists(),
248
- "Associated files": list(Path(shapefile_path).parent.glob("*.*")),
249
- }
250
- )
251
-
252
- return plot_seasonal_salinity_for_bays(
253
- data,
254
- year,
255
- shapefile_path=shapefile_path,
256
- wbids=wbids,
257
- reporting_end_month=st.session_state.reporting_month,
258
  )
 
8
  import pandas as pd
9
  import streamlit as st
10
  from matplotlib.colors import LinearSegmentedColormap
11
+ from matplotlib.figure import Figure
12
  from osgeo import gdal
13
 
14
  from utils.data_loading import timer
 
214
 
215
 
216
  @timer(include_params=True)
217
+ def generate_seasonal_plot(
218
+ data: pd.DataFrame,
219
+ parameter: str,
220
+ year: str,
221
+ shapefile_path: str,
222
+ reporting_end_month: int = 10,
223
+ basemap_provider=ctx.providers.USGS.USTopo, # type: ignore
224
+ alpha: float = 0.5,
225
+ ) -> tuple[Figure, pd.DataFrame, pd.DataFrame]:
226
+ """
227
+ Create seasonal plots of mean parameter values by WBID for specific bays.
228
+
229
+ Parameters
230
+ ----------
231
+ data : pd.DataFrame
232
+ DataFrame containing measurements with lat/long
233
+ parameter : str
234
+ Parameter to plot (e.g., "Salinity", "Dissolved Oxygen")
235
+ year : str
236
+ Reporting Year to filter data for
237
+ shapefile_path : str
238
+ Path to WBID shapefile
239
+ reporting_end_month : int
240
+ Last month of reporting year (1-12)
241
+ basemap_provider : ctx.providers
242
+ Contextily map provider
243
+ alpha : float
244
+ Transparency of basemap
245
+
246
+ Returns
247
+ -------
248
+ tuple[Figure, pd.DataFrame, pd.DataFrame]
249
+ - Figure: Matplotlib figure containing the plot
250
+ - DataFrame: Raw data used in plot
251
+ - DataFrame: Processed quarterly means
252
+ """
253
+ if st.session_state.get("DEBUG", False):
254
+ debugging_info(data, shapefile_path)
255
+
256
+ # Load WBIDs
257
+ wbids = gpd.read_file(shapefile_path)
258
+ if wbids.crs is None:
259
+ wbids.set_crs(epsg=6439, inplace=True)
260
+ wbids = wbids.to_crs(epsg=3857)
261
+
262
+ # Filter for specific bays
263
+ bay_wbids = [
264
+ "1061A",
265
+ "1061B",
266
+ "1061C",
267
+ "1061D",
268
+ "1061E",
269
+ "1061F",
270
+ "1061G",
271
+ "1061H",
272
+ "1055A",
273
+ ]
274
+ year_data = data[
275
+ (data["Reporting_Year"] == int(year)) & (data["WBID"].isin(bay_wbids))
276
+ ].copy()
277
+
278
+ # Calculate quarterly means
279
+ seasonal_means = calculate_quarterly_means(
280
+ year_data, parameter, reporting_end_month
281
+ )
282
+
283
+ # Create the plot
284
+ fig = create_quarterly_maps(
285
+ seasonal_means=seasonal_means,
286
+ wbids=wbids[wbids["WBID"].isin(bay_wbids)],
287
+ parameter=parameter,
288
+ year=year,
289
+ reporting_end_month=reporting_end_month,
290
+ basemap_provider=basemap_provider,
291
+ alpha=alpha,
292
+ )
293
+
294
+ return fig, year_data, seasonal_means
295
+
296
+
297
+ def calculate_quarterly_means(
298
+ data: pd.DataFrame, parameter: str, reporting_end_month: int
299
+ ) -> pd.DataFrame:
300
+ """Calculate quarterly means for the parameter"""
301
+ # Add quarter information
302
+ data["quarter"] = data["Activity_Start_Date_Time"].apply(
303
+ lambda x: get_quarter(x, reporting_end_month)
304
+ )
305
+
306
+ # Calculate means
307
+ return (
308
+ data.groupby(["WBID", "quarter"], observed=True)["Org_Result_Value"]
309
+ .mean()
310
+ .reset_index()
311
+ .rename(columns={"Org_Result_Value": parameter})
312
+ )
313
+
314
+
315
+ def get_quarter(date, reporting_end_month: int) -> str:
316
+ """Calculate quarter based on reporting year end month"""
317
+ month = date.month
318
+ month_offset = (12 - reporting_end_month) % 12
319
+ adjusted_month = ((month + month_offset) % 12) or 12
320
+ return f"Q{((adjusted_month - 1) // 3) + 1}"
321
+
322
+
323
+ def create_quarterly_maps(
324
+ seasonal_means: pd.DataFrame,
325
+ wbids: gpd.GeoDataFrame,
326
+ parameter: str,
327
+ year: str,
328
+ reporting_end_month: int,
329
+ basemap_provider,
330
+ alpha: float = 0.5,
331
+ ) -> Figure:
332
+ """Create the quarterly map visualization"""
333
+ fig = plt.figure(figsize=(20, 14))
334
+
335
+ # Set up color scheme
336
+ colors = ["#08519c", "#73a9cf", "#fee090", "#fc8d59", "#d73027"]
337
+ cmap = LinearSegmentedColormap.from_list("custom", colors, N=100)
338
+
339
+ # Calculate plot bounds
340
+ bounds = wbids.total_bounds
341
+ extent = calculate_map_extent(bounds)
342
+
343
+ # Create grid layout with tight spacing
344
+ gs = fig.add_gridspec(
345
+ 2,
346
+ 2,
347
+ width_ratios=[1, 1],
348
+ wspace=0.05,
349
+ hspace=-0.15,
350
+ left=0.02,
351
+ right=0.98,
352
+ top=0.95,
353
+ bottom=0.05,
354
+ )
355
+
356
+ # Plot each quarter
357
+ for idx, quarter in enumerate(["Q1", "Q2", "Q3", "Q4"]):
358
+ ax = fig.add_subplot(gs[idx // 2, idx % 2])
359
+ plot_quarter(
360
+ ax=ax,
361
+ quarter=quarter,
362
+ seasonal_means=seasonal_means,
363
+ wbids=wbids,
364
+ parameter=parameter,
365
+ year=year,
366
+ reporting_end_month=reporting_end_month,
367
+ cmap=cmap,
368
+ extent=extent,
369
+ basemap_provider=basemap_provider,
370
+ alpha=alpha,
371
+ )
372
+
373
+ add_colorbar(fig, seasonal_means, parameter, cmap)
374
+
375
+ return fig
376
+
377
+
378
+ def plot_quarter(
379
+ ax: plt.Axes, # type: ignore
380
+ quarter: str,
381
+ seasonal_means: pd.DataFrame,
382
+ wbids: gpd.GeoDataFrame,
383
+ parameter: str,
384
+ year: str,
385
+ reporting_end_month: int,
386
+ cmap: LinearSegmentedColormap,
387
+ extent: list[float],
388
+ basemap_provider,
389
+ alpha: float = 0.5,
390
+ ) -> None:
391
+ """Plot a single quarter's map"""
392
+ # Get data for this quarter
393
+ quarter_data = seasonal_means[seasonal_means["quarter"] == quarter]
394
+ merged = wbids.merge(quarter_data, on="WBID", how="left")
395
+
396
+ # Get value range for consistent colormap
397
+ vmin = seasonal_means[parameter].min()
398
+ vmax = get_parameter_max_value(parameter, seasonal_means[parameter].max())
399
+
400
+ # Plot WBIDs
401
+ merged.plot(
402
+ column=parameter,
403
+ ax=ax,
404
+ cmap=cmap,
405
+ vmin=vmin,
406
+ vmax=vmax,
407
+ alpha=0.7,
408
+ missing_kwds={"color": "lightgrey", "alpha": 0.5},
409
+ )
410
+
411
+ # Add basemap
412
+ ctx.add_basemap(ax, source=basemap_provider, zoom=11, alpha=alpha) # type: ignore
413
+
414
+ # Set map extent
415
+ ax.set_xlim(extent[0], extent[1])
416
+ ax.set_ylim(extent[2], extent[3])
417
+
418
+ # Get date range for this quarter
419
+ date_range = get_quarter_dates(quarter, int(year), reporting_end_month)
420
+
421
+ # Create title with appropriate padding based on position
422
+ title_pad = 15 if int(quarter[1]) <= 2 else 5 # Top row vs bottom row
423
+ ax.set_title(
424
+ f"Quarter {quarter[1]} Mean {parameter}\n{date_range}",
425
+ pad=title_pad,
426
+ fontsize=10,
427
+ )
428
+ ax.set_axis_off()
429
+
430
+
431
+ def get_parameter_max_value(parameter: str, data_max: float) -> float:
432
+ """Get the maximum value for colormap scaling based on parameter"""
433
+ parameter_limits = {
434
+ "Salinity": 40,
435
+ "Dissolved Oxygen": 12,
436
+ "pH": 9,
437
+ "Temperature, Water": 35,
438
+ "Turbidity": None, # Use data max
439
+ "Total Nitrogen": None,
440
+ "Total Phosphorus": None,
441
+ "Fecal Coliform (MPN)": None,
442
+ }
443
+ return parameter_limits.get(parameter, data_max)
444
+
445
+
446
+ def calculate_map_extent(
447
+ bounds: np.ndarray, buffer_fraction: float = 0.05
448
+ ) -> list[float]:
449
+ """Calculate map extent with buffer"""
450
+ x_buffer = (bounds[2] - bounds[0]) * buffer_fraction
451
+ y_buffer = (bounds[3] - bounds[1]) * buffer_fraction
452
+ return [
453
+ bounds[0] - x_buffer, # xmin
454
+ bounds[2] + x_buffer, # xmax
455
+ bounds[1] - y_buffer, # ymin
456
+ bounds[3] + y_buffer, # ymax
457
+ ]
458
+
459
+
460
+ def get_quarter_dates(quarter: str, year: int, reporting_end_month: int) -> str:
461
+ """Get date range string for a quarter"""
462
+ # Calculate first month of reporting year
463
+ first_month = (reporting_end_month % 12) + 1
464
+
465
+ # Calculate start month for each quarter
466
+ quarter_num = int(quarter[1])
467
+ start_month = ((first_month - 1 + ((quarter_num - 1) * 3)) % 12) + 1
468
+ end_month = ((start_month + 2) % 12) or 12
469
+
470
+ # Determine correct years for start and end dates
471
+ start_year = year - 1 if start_month > reporting_end_month else year
472
+ end_year = start_year if end_month >= start_month else start_year + 1
473
+
474
+ # Create date objects
475
+ start_date = pd.Timestamp(f"{start_year}-{start_month:02d}-01")
476
+ end_date = pd.Timestamp(
477
+ f"{end_year}-{end_month:02d}-{pd.Timestamp(f'{end_year}-{end_month:02d}').days_in_month}"
478
+ )
479
+
480
+ return f"{start_date.strftime('%b %d, %Y')} - {end_date.strftime('%b %d, %Y')}"
481
+
482
+
483
+ def add_colorbar(
484
+ fig: Figure,
485
+ seasonal_means: pd.DataFrame,
486
+ parameter: str,
487
+ cmap: LinearSegmentedColormap,
488
+ ) -> None:
489
+ """Add colorbar to the figure"""
490
+ # Get value range
491
+ vmin = seasonal_means[parameter].min()
492
+ vmax = get_parameter_max_value(parameter, seasonal_means[parameter].max())
493
+
494
+ # Create colorbar
495
+ norm = plt.Normalize(vmin=vmin, vmax=vmax) # type: ignore
496
+ sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
497
+ sm.set_array([])
498
+
499
+ # Get parameter unit
500
+ unit = get_parameter_unit(parameter)
501
+ label = f"{parameter} ({unit})" if unit else parameter
502
+
503
+ # Add colorbar to figure
504
+ fig.colorbar(
505
+ sm,
506
+ ax=fig.axes,
507
+ orientation="vertical",
508
+ label=label,
509
+ pad=0.01,
510
+ fraction=0.015,
511
+ ticks=np.arange(0, vmax + 5, 5), # Add ticks every 5 units
512
+ )
513
+
514
+
515
+ def get_parameter_unit(parameter: str) -> str:
516
+ """Get the unit for a parameter"""
517
+ parameter_units = {
518
+ "Salinity": "ppt",
519
+ "Dissolved Oxygen": "mg/L",
520
+ "pH": "",
521
+ "Temperature, Water": "°C",
522
+ "Turbidity": "NTU",
523
+ "Total Nitrogen": "mg/L",
524
+ "Total Phosphorus": "mg/L",
525
+ "Fecal Coliform (MPN)": "MPN/100mL",
526
+ }
527
+ return parameter_units.get(parameter, "")
528
+
529
+
530
+ def debugging_info(data: pd.DataFrame, shapefile_path: str) -> None:
531
  # Add debugging information
532
  wbids = gpd.read_file(shapefile_path)
533
 
 
544
  # Pre-transform to Web Mercator (EPSG:3857) here to avoid issues in plotting function
545
  wbids = wbids.to_crs(epsg=3857)
546
 
547
+ st.write("Debug Info:")
548
+ st.write(
549
+ {
550
+ "Shapefile CRS": wbids.crs,
551
+ "Input Data CRS": data.crs
552
+ if isinstance(data, gpd.GeoDataFrame)
553
+ else "Not a GeoDataFrame",
554
+ "GDAL Version": gdal.VersionInfo()
555
+ if "osgeo.gdal" in sys.modules
556
+ else "Not available",
557
+ "GeoPandas Version": gpd.__version__,
558
+ "Python Version": sys.version,
559
+ "File exists": Path(shapefile_path).exists(),
560
+ "Associated files": list(Path(shapefile_path).parent.glob("*.*")),
561
+ }
 
 
 
 
 
 
 
 
 
562
  )
ui/pages/home.py CHANGED
@@ -1,6 +1,5 @@
1
  import io
2
 
3
- import pandas as pd
4
  import streamlit as st
5
 
6
  from components import render_stations_map
 
1
  import io
2
 
 
3
  import streamlit as st
4
 
5
  from components import render_stations_map
ui/pages/seasonal_maps.py CHANGED
@@ -1,33 +1,90 @@
 
 
1
  import streamlit as st
2
 
 
3
  from dashboard_analytics import log_visit
4
  from plots.map import generate_seasonal_plot
5
- from utils.data_loading import load_seasonal_data
6
 
7
  log_visit("Seasonal Maps")
8
 
9
-
10
  st.title("Seasonal Variations")
11
  raw_df = st.session_state.data["raw_df"]
12
- # Use Reporting_Year instead of calendar year
13
- years = sorted(raw_df["Reporting_Year"].unique())
14
 
15
  # Move filters to sidebar
16
  st.sidebar.markdown("### Filter Options")
17
- analyte = st.sidebar.selectbox(
18
- "Select Parameter:", ["Salinity"], index=0, key="seasonal_analyte_select"
 
 
 
 
 
 
19
  )
 
 
 
20
  selected_year = st.sidebar.selectbox(
21
- "Select Year:", sorted(years, reverse=True), index=0, key="seasonal_year_select"
22
  )
23
 
24
  if not raw_df.empty:
25
- seasonal_data = load_seasonal_data(raw_df, analyte)
26
- fig = generate_seasonal_plot(
27
- seasonal_data,
28
- str(selected_year),
 
 
 
 
 
 
 
29
  shapefile_path="data/waterbody_ids/Waterbody_IDs_(WBIDs).shp",
 
30
  )
31
  st.pyplot(fig)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  else:
33
  st.warning("No data available for seasonal analysis.")
 
1
+ import io
2
+
3
  import streamlit as st
4
 
5
+ from components import render_filtered_data_preview
6
  from dashboard_analytics import log_visit
7
  from plots.map import generate_seasonal_plot
8
+ from utils.data_loading import add_lat_long, get_stations_data
9
 
10
  log_visit("Seasonal Maps")
11
 
 
12
  st.title("Seasonal Variations")
13
  raw_df = st.session_state.data["raw_df"]
 
 
14
 
15
  # Move filters to sidebar
16
  st.sidebar.markdown("### Filter Options")
17
+
18
+ # Allow selection of any parameter
19
+ parameters = sorted(raw_df["Org_Analyte_Name"].unique())
20
+ selected_parameter = st.sidebar.selectbox(
21
+ "Select Parameter:",
22
+ parameters,
23
+ index=parameters.index("Salinity") if "Salinity" in parameters else 0,
24
+ key="seasonal_parameter_select",
25
  )
26
+
27
+ # Use Reporting_Year for consistency
28
+ years = sorted(raw_df["Reporting_Year"].unique(), reverse=True)
29
  selected_year = st.sidebar.selectbox(
30
+ "Select Year:", years, index=0, key="seasonal_year_select"
31
  )
32
 
33
  if not raw_df.empty:
34
+ # Filter data for selected parameter
35
+ param_data = raw_df[raw_df["Org_Analyte_Name"] == selected_parameter].copy()
36
+
37
+ # Add lat/long information using existing function
38
+ param_data = add_lat_long(param_data, get_stations_data())
39
+
40
+ # Generate plot and get data
41
+ fig, raw_data, plot_data = generate_seasonal_plot(
42
+ data=param_data,
43
+ parameter=selected_parameter,
44
+ year=str(selected_year),
45
  shapefile_path="data/waterbody_ids/Waterbody_IDs_(WBIDs).shp",
46
+ reporting_end_month=st.session_state.reporting_month,
47
  )
48
  st.pyplot(fig)
49
+
50
+ # Add data viewers
51
+ with st.expander("Chart Data"):
52
+ render_filtered_data_preview(
53
+ plot_data, display_columns=plot_data.columns.tolist()
54
+ )
55
+
56
+ # Add CSV download button for chart data
57
+ csv_buffer = io.StringIO()
58
+ plot_data.to_csv(csv_buffer, index=False)
59
+ st.download_button(
60
+ label=f"Download Chart Data for {selected_parameter} (CSV)",
61
+ data=csv_buffer.getvalue(),
62
+ file_name=f"{selected_parameter}_seasonal_{selected_year}_chart_data.csv",
63
+ mime="text/csv",
64
+ )
65
+
66
+ with st.expander("Raw Data"):
67
+ render_filtered_data_preview(
68
+ raw_data,
69
+ [
70
+ "Activity_Start_Date_Time",
71
+ "Reporting_Year",
72
+ "WBID",
73
+ "Station_Number",
74
+ "Sample_Position",
75
+ "Org_Analyte_Name",
76
+ "Org_Result_Value",
77
+ ],
78
+ )
79
+
80
+ # Add CSV download button for raw data
81
+ csv_buffer = io.StringIO()
82
+ raw_data.to_csv(csv_buffer, index=False)
83
+ st.download_button(
84
+ label=f"Download Raw Data for {selected_parameter} (CSV)",
85
+ data=csv_buffer.getvalue(),
86
+ file_name=f"{selected_parameter}_seasonal_{selected_year}_raw_data.csv",
87
+ mime="text/csv",
88
+ )
89
  else:
90
  st.warning("No data available for seasonal analysis.")