harshini9942 commited on
Commit
231c749
·
1 Parent(s): 78f8034

streamlit changes

Browse files
Files changed (2) hide show
  1. streamlit/dashboard.py +148 -24
  2. streamlit/plot_utils.py +91 -45
streamlit/dashboard.py CHANGED
@@ -505,6 +505,11 @@ with st.spinner("🌊 Initialising ARGO Dashboard …"):
505
  # Derived column: is this float a BGC float?
506
  df_prof["is_bgc"] = df_prof["wmo_id"].isin(bgc_wmos)
507
 
 
 
 
 
 
508
  # Enrich profiles with profiler_name from meta (authoritative per-float source)
509
  _meta_pname = df_meta.set_index("wmo_id")["profiler_name"]
510
  df_prof["profiler_name"] = df_prof["wmo_id"].map(_meta_pname).fillna("Unknown")
@@ -761,31 +766,38 @@ def show_float_details(wmo):
761
  cycles, dates, pres, temp, psal, rho = plot_utils.get_valid_data(ds_prof)
762
 
763
  if len(pres) > 0:
 
 
 
 
 
 
 
764
  c1, c2, c3 = st.columns(3)
765
  with c1:
766
- fig = plot_utils.create_ts_diagram(cycles, temp, psal, wmo)
767
  st.plotly_chart(fig, use_container_width=True)
768
  with c2:
769
- fig = plot_utils.create_section_chart(dates, pres, temp, "Temperature (°C)", "Section chart TEMP", wmo)
770
  st.plotly_chart(fig, use_container_width=True)
771
  with c3:
772
- fig = plot_utils.create_section_chart(dates, pres, psal, "Salinity (PSU)", "Section chart PSAL", wmo)
773
  st.plotly_chart(fig, use_container_width=True)
774
 
775
  c4, c5, c6 = st.columns(3)
776
  with c4:
777
- fig = plot_utils.create_section_chart(dates, pres, rho, "Potential Density (kg/m³)", "Section chart RHO", wmo)
778
  st.plotly_chart(fig, use_container_width=True)
779
  with c5:
780
- fig = plot_utils.create_overlaid_profiles(temp, pres, cycles, "Temperature (°C)", "Overlaid profiles TEMP", wmo)
781
  st.plotly_chart(fig, use_container_width=True)
782
  with c6:
783
- fig = plot_utils.create_overlaid_profiles(psal, pres, cycles, "Salinity (PSU)", "Overlaid profiles PSAL", wmo)
784
  st.plotly_chart(fig, use_container_width=True)
785
 
786
  c7, c8, c9 = st.columns(3)
787
  with c7:
788
- fig = plot_utils.create_overlaid_profiles(rho, pres, cycles, "Potential Density (kg/m³)", "Overlaid profiles RHO", wmo)
789
  st.plotly_chart(fig, use_container_width=True)
790
  else:
791
  st.info("No valid profile data available for technical plots.")
@@ -838,6 +850,17 @@ with st.sidebar:
838
  help="Comma-separated WMO numbers",
839
  )
840
 
 
 
 
 
 
 
 
 
 
 
 
841
  # ── QC Mode ──
842
  _qc_options = ["All", "Delayed", "Real time"]
843
  _qc_default = _qc_options.index(qp.get("qc", "All")) if qp.get("qc", "All") in _qc_options else 0
@@ -1023,6 +1046,19 @@ def apply_filters(df, *, is_bio=False):
1023
  if selected_profiler_types and "profiler_name" in out.columns:
1024
  out = out[out["profiler_name"].isin(selected_profiler_types)]
1025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1026
  return out
1027
 
1028
 
@@ -1065,7 +1101,14 @@ col_left, col_right = st.columns([55, 45], gap="medium")
1065
  with col_left:
1066
  # ── Component 1: Geospatial Float Position Map (PRD §7.1) ──
1067
  st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1068
- st.markdown("### 📍 Geographic Float Positions")
 
 
 
 
 
 
 
1069
 
1070
  if len(filt_prof) > 0:
1071
  # --- Check map selection from session state ---
@@ -1080,12 +1123,23 @@ with col_left:
1080
  is_sidebar_search = bool(search_wmo.strip())
1081
  is_wmo_searched = is_sidebar_search or bool(selected_wmo_from_map)
1082
 
 
 
 
 
 
 
 
 
1083
  # Apply Live-Only filter if toggled and not searching specific WMOs
1084
  map_source = filt_prof.copy()
1085
 
1086
  # If user clicked a float on the map, filter source to just that float
1087
  if selected_wmo_from_map:
1088
  map_source = map_source[map_source["wmo_id"] == selected_wmo_from_map]
 
 
 
1089
 
1090
  if show_live_only and not is_wmo_searched:
1091
  latest_d = map_source["date"].max()
@@ -1137,7 +1191,17 @@ with col_left:
1137
  plot_bgcolor="rgba(0,0,0,0)",
1138
  margin=dict(l=0, r=0, t=0, b=0),
1139
  mapbox=dict(
1140
- style="carto-darkmatter",
 
 
 
 
 
 
 
 
 
 
1141
  center=dict(lat=center_lat, lon=center_lon),
1142
  zoom=4
1143
  ),
@@ -1187,7 +1251,17 @@ with col_left:
1187
  )
1188
  fig_map.update_traces(marker=dict(size=8, opacity=0.9))
1189
  fig_map.update_layout(
1190
- mapbox_style="carto-darkmatter",
 
 
 
 
 
 
 
 
 
 
1191
  paper_bgcolor="rgba(0,0,0,0)",
1192
  plot_bgcolor="rgba(0,0,0,0)",
1193
  margin=dict(l=0, r=0, t=0, b=0),
@@ -1228,7 +1302,14 @@ with col_left:
1228
  with col_right:
1229
  st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1230
  # ── Bar chart (PRD §7.2) ──
1231
- st.markdown("### 📈 Number of Floats per DAC")
 
 
 
 
 
 
 
1232
 
1233
  if len(filt_prof) > 0 and "dac" in filt_prof.columns:
1234
  # Active floats in the last 90 days of each year
@@ -1266,6 +1347,7 @@ with col_right:
1266
  x="Year",
1267
  y="Count",
1268
  color="DAC",
 
1269
  color_discrete_map=DAC_COLORS,
1270
  category_orders={"Year": sorted(yearly["Year"].unique())}
1271
  )
@@ -1297,7 +1379,7 @@ with col_right:
1297
  margin=dict(l=50, r=20, t=80, b=40),
1298
  )
1299
  )
1300
- st.plotly_chart(fig_bar, use_container_width=True, key="bar_chart", config={"toImageButtonOptions": {"format": "png", "scale": 2, "filename": "argo_annual_floats"}})
1301
  else:
1302
  st.info("No active float data for bar chart.")
1303
  else:
@@ -1622,6 +1704,26 @@ if len(df_prof) > 0:
1622
  dac = dac.sort_values("Profiles", ascending=False)
1623
 
1624
  dacs = dac["institution"].tolist()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1625
  header = "".join(f"<th>{d}</th>" for d in dacs)
1626
  floats_cells = "".join(f"<td>{int(r):,}</td>" for r in dac["Floats"])
1627
  profs_cells = "".join(f"<td>{int(r):,}</td>" for r in dac["Profiles"])
@@ -1645,20 +1747,42 @@ if len(df_prof) > 0:
1645
  )
1646
  st.markdown('</div>', unsafe_allow_html=True)
1647
 
1648
- with col_dac2:
1649
- st.markdown("### 📡 Float Status Summary")
1650
- latest_date = df_prof["date"].max()
1651
- ninety_days_ago = pd.Timestamp(latest_date - timedelta(days=90))
1652
- float_latest = df_prof.dropna(subset=["date"]).groupby(["institution", "wmo_id"])["date"].max().reset_index()
1653
- float_latest["is_live"] = float_latest["date"] >= ninety_days_ago
1654
 
1655
- live_df = float_latest.groupby("institution").agg(
1656
- live_floats=("is_live", "sum")
1657
- ).reset_index()
 
 
1658
 
1659
- status_df = pd.merge(dac_floats.rename(columns={"Floats": "total_count"}), live_df, on="institution", how="left").fillna(0)
1660
- status_df["dead_floats"] = status_df["total_count"] - status_df["live_floats"]
1661
- status_df = status_df.set_index("institution").reindex(dacs).reset_index().fillna(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1662
 
1663
  # Dominant instrument per institution from meta registry
1664
  _inst_top_model = (
 
505
  # Derived column: is this float a BGC float?
506
  df_prof["is_bgc"] = df_prof["wmo_id"].isin(bgc_wmos)
507
 
508
+ wmos_with_doxy = set(df_bio[df_bio["has_doxy"]]["wmo_id"].dropna().unique()) if "has_doxy" in df_bio.columns else set()
509
+ wmos_with_chla = set(df_bio[df_bio["has_chla"]]["wmo_id"].dropna().unique()) if "has_chla" in df_bio.columns else set()
510
+ wmos_with_nitrate = set(df_bio[df_bio["has_nitrate"]]["wmo_id"].dropna().unique()) if "has_nitrate" in df_bio.columns else set()
511
+ wmos_with_ph = set(df_bio[df_bio["has_ph"]]["wmo_id"].dropna().unique()) if "has_ph" in df_bio.columns else set()
512
+
513
  # Enrich profiles with profiler_name from meta (authoritative per-float source)
514
  _meta_pname = df_meta.set_index("wmo_id")["profiler_name"]
515
  df_prof["profiler_name"] = df_prof["wmo_id"].map(_meta_pname).fillna("Unknown")
 
766
  cycles, dates, pres, temp, psal, rho = plot_utils.get_valid_data(ds_prof)
767
 
768
  if len(pres) > 0:
769
+ if len(dates) > 0:
770
+ min_date = pd.to_datetime(np.nanmin(dates)).strftime('%d/%m/%Y')
771
+ max_date = pd.to_datetime(np.nanmax(dates)).strftime('%d/%m/%Y')
772
+ date_suffix = f"Argo float {wmo} between {min_date} and {max_date}"
773
+ else:
774
+ date_suffix = f"Argo float {wmo}"
775
+
776
  c1, c2, c3 = st.columns(3)
777
  with c1:
778
+ fig = plot_utils.create_ts_diagram(cycles, temp, psal, wmo, title=f"T/S Diagram<br><sup>{date_suffix}</sup>")
779
  st.plotly_chart(fig, use_container_width=True)
780
  with c2:
781
+ fig = plot_utils.create_section_chart(dates, pres, temp, "Temperature (°C)", f"Section chart TEMP<br><sup>{date_suffix}</sup>", wmo)
782
  st.plotly_chart(fig, use_container_width=True)
783
  with c3:
784
+ fig = plot_utils.create_section_chart(dates, pres, psal, "Salinity (PSU)", f"Section chart PSAL<br><sup>{date_suffix}</sup>", wmo)
785
  st.plotly_chart(fig, use_container_width=True)
786
 
787
  c4, c5, c6 = st.columns(3)
788
  with c4:
789
+ fig = plot_utils.create_section_chart(dates, pres, rho, "Potential Density (kg/m³)", f"Section chart RHO<br><sup>{date_suffix}</sup>", wmo)
790
  st.plotly_chart(fig, use_container_width=True)
791
  with c5:
792
+ fig = plot_utils.create_overlaid_profiles(temp, pres, cycles, "Temperature (°C)", f"Overlaid profiles TEMP<br><sup>{date_suffix}</sup>", wmo)
793
  st.plotly_chart(fig, use_container_width=True)
794
  with c6:
795
+ fig = plot_utils.create_overlaid_profiles(psal, pres, cycles, "Salinity (PSU)", f"Overlaid profiles PSAL<br><sup>{date_suffix}</sup>", wmo)
796
  st.plotly_chart(fig, use_container_width=True)
797
 
798
  c7, c8, c9 = st.columns(3)
799
  with c7:
800
+ fig = plot_utils.create_overlaid_profiles(rho, pres, cycles, "Potential Density (kg/m³)", f"Overlaid profiles RHO<br><sup>{date_suffix}</sup>", wmo)
801
  st.plotly_chart(fig, use_container_width=True)
802
  else:
803
  st.info("No valid profile data available for technical plots.")
 
850
  help="Comma-separated WMO numbers",
851
  )
852
 
853
+ # ── Parameter Filter ──
854
+ st.markdown("### Parameters")
855
+ _param_options = ["Pressure", "Temperature", "Salinity", "Oxygen (DOXY)", "Chlorophyll (Chla)", "Nitrate", "pH"]
856
+ selected_params = st.multiselect(
857
+ "🔎 Search by Parameters",
858
+ options=_param_options,
859
+ default=[],
860
+ placeholder="Select parameters",
861
+ help="Filter floats that have these parameters"
862
+ )
863
+
864
  # ── QC Mode ──
865
  _qc_options = ["All", "Delayed", "Real time"]
866
  _qc_default = _qc_options.index(qp.get("qc", "All")) if qp.get("qc", "All") in _qc_options else 0
 
1046
  if selected_profiler_types and "profiler_name" in out.columns:
1047
  out = out[out["profiler_name"].isin(selected_profiler_types)]
1048
 
1049
+ # Parameter filtering
1050
+ if selected_params:
1051
+ wmo_mask = pd.Series(True, index=out.index)
1052
+ if "Oxygen (DOXY)" in selected_params:
1053
+ wmo_mask &= out["wmo_id"].isin(wmos_with_doxy)
1054
+ if "Chlorophyll (Chla)" in selected_params:
1055
+ wmo_mask &= out["wmo_id"].isin(wmos_with_chla)
1056
+ if "Nitrate" in selected_params:
1057
+ wmo_mask &= out["wmo_id"].isin(wmos_with_nitrate)
1058
+ if "pH" in selected_params:
1059
+ wmo_mask &= out["wmo_id"].isin(wmos_with_ph)
1060
+ out = out[wmo_mask]
1061
+
1062
  return out
1063
 
1064
 
 
1101
  with col_left:
1102
  # ── Component 1: Geospatial Float Position Map (PRD §7.1) ──
1103
  st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1104
+ c_title, c_btn = st.columns([7, 3])
1105
+ with c_title:
1106
+ st.markdown("### 📍 Geographic Float Positions")
1107
+ with c_btn:
1108
+ if st.button("🏠 Reset Map Data", use_container_width=True):
1109
+ if "main_map" in st.session_state:
1110
+ del st.session_state["main_map"]
1111
+ st.rerun()
1112
 
1113
  if len(filt_prof) > 0:
1114
  # --- Check map selection from session state ---
 
1123
  is_sidebar_search = bool(search_wmo.strip())
1124
  is_wmo_searched = is_sidebar_search or bool(selected_wmo_from_map)
1125
 
1126
+ selected_dac_from_bar = None
1127
+ if "bar_chart" in st.session_state:
1128
+ sel = st.session_state.bar_chart
1129
+ if sel and "selection" in sel and "points" in sel["selection"] and len(sel["selection"]["points"]) > 0:
1130
+ pt = sel["selection"]["points"][0]
1131
+ if "customdata" in pt and len(pt["customdata"]) > 0:
1132
+ selected_dac_from_bar = str(pt["customdata"][0])
1133
+
1134
  # Apply Live-Only filter if toggled and not searching specific WMOs
1135
  map_source = filt_prof.copy()
1136
 
1137
  # If user clicked a float on the map, filter source to just that float
1138
  if selected_wmo_from_map:
1139
  map_source = map_source[map_source["wmo_id"] == selected_wmo_from_map]
1140
+
1141
+ if selected_dac_from_bar:
1142
+ map_source = map_source[map_source["dac"] == selected_dac_from_bar]
1143
 
1144
  if show_live_only and not is_wmo_searched:
1145
  latest_d = map_source["date"].max()
 
1191
  plot_bgcolor="rgba(0,0,0,0)",
1192
  margin=dict(l=0, r=0, t=0, b=0),
1193
  mapbox=dict(
1194
+ style="white-bg",
1195
+ layers=[
1196
+ {
1197
+ "below": 'traces',
1198
+ "sourcetype": "raster",
1199
+ "sourceattribution": "Esri",
1200
+ "source": [
1201
+ "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
1202
+ ]
1203
+ }
1204
+ ],
1205
  center=dict(lat=center_lat, lon=center_lon),
1206
  zoom=4
1207
  ),
 
1251
  )
1252
  fig_map.update_traces(marker=dict(size=8, opacity=0.9))
1253
  fig_map.update_layout(
1254
+ mapbox_style="white-bg",
1255
+ mapbox_layers=[
1256
+ {
1257
+ "below": 'traces',
1258
+ "sourcetype": "raster",
1259
+ "sourceattribution": "Esri",
1260
+ "source": [
1261
+ "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
1262
+ ]
1263
+ }
1264
+ ],
1265
  paper_bgcolor="rgba(0,0,0,0)",
1266
  plot_bgcolor="rgba(0,0,0,0)",
1267
  margin=dict(l=0, r=0, t=0, b=0),
 
1302
  with col_right:
1303
  st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1304
  # ── Bar chart (PRD §7.2) ──
1305
+ c_bar_title, c_bar_btn = st.columns([7, 3])
1306
+ with c_bar_title:
1307
+ st.markdown("### 📈 Number of Floats per DAC")
1308
+ with c_bar_btn:
1309
+ if st.button("🏠 Reset Chart Data", use_container_width=True):
1310
+ if "bar_chart" in st.session_state:
1311
+ del st.session_state["bar_chart"]
1312
+ st.rerun()
1313
 
1314
  if len(filt_prof) > 0 and "dac" in filt_prof.columns:
1315
  # Active floats in the last 90 days of each year
 
1347
  x="Year",
1348
  y="Count",
1349
  color="DAC",
1350
+ custom_data=["DAC"],
1351
  color_discrete_map=DAC_COLORS,
1352
  category_orders={"Year": sorted(yearly["Year"].unique())}
1353
  )
 
1379
  margin=dict(l=50, r=20, t=80, b=40),
1380
  )
1381
  )
1382
+ st.plotly_chart(fig_bar, use_container_width=True, key="bar_chart", on_select="rerun", config={"toImageButtonOptions": {"format": "png", "scale": 2, "filename": "argo_annual_floats"}})
1383
  else:
1384
  st.info("No active float data for bar chart.")
1385
  else:
 
1704
  dac = dac.sort_values("Profiles", ascending=False)
1705
 
1706
  dacs = dac["institution"].tolist()
1707
+
1708
+ latest_date = df_prof["date"].max()
1709
+ ninety_days_ago = pd.Timestamp(latest_date - timedelta(days=90))
1710
+ float_latest = df_prof.dropna(subset=["date"]).groupby(["institution", "wmo_id"])["date"].max().reset_index()
1711
+ float_latest["is_live"] = float_latest["date"] >= ninety_days_ago
1712
+
1713
+ live_df = float_latest.groupby("institution").agg(
1714
+ live_floats=("is_live", "sum")
1715
+ ).reset_index()
1716
+
1717
+ status_df = pd.merge(dac_floats.rename(columns={"Floats": "total_count"}), live_df, on="institution", how="left").fillna(0)
1718
+ status_df["dead_floats"] = status_df["total_count"] - status_df["live_floats"]
1719
+ status_df = status_df.set_index("institution").reindex(dacs).reset_index().fillna(0)
1720
+
1721
+ # Global metrics for the graph
1722
+ global_total_floats = status_df["total_count"].sum()
1723
+ global_total_profiles = dac["Profiles"].sum()
1724
+ global_live = status_df["live_floats"].sum()
1725
+ global_dead = status_df["dead_floats"].sum()
1726
+
1727
  header = "".join(f"<th>{d}</th>" for d in dacs)
1728
  floats_cells = "".join(f"<td>{int(r):,}</td>" for r in dac["Floats"])
1729
  profs_cells = "".join(f"<td>{int(r):,}</td>" for r in dac["Profiles"])
 
1747
  )
1748
  st.markdown('</div>', unsafe_allow_html=True)
1749
 
1750
+ st.markdown("<br>", unsafe_allow_html=True)
1751
+ st.markdown("### 🌐 Global Network Status")
 
 
 
 
1752
 
1753
+ metrics_df = pd.DataFrame({
1754
+ "Metric": ["Active Floats", "Dead Floats", "Total Floats", "Total Profiles"],
1755
+ "Count": [global_live, global_dead, global_total_floats, global_total_profiles],
1756
+ "Color": ["#4CAF50", "#F44336", "#9C27B0", "#2196F3"]
1757
+ })
1758
 
1759
+ fig_global = px.bar(
1760
+ metrics_df,
1761
+ x="Count",
1762
+ y="Metric",
1763
+ orientation="h",
1764
+ text="Count",
1765
+ log_x=True,
1766
+ )
1767
+ fig_global.update_traces(
1768
+ marker_color=metrics_df["Color"],
1769
+ texttemplate='<b>%{text:,}</b>',
1770
+ textposition='auto',
1771
+ textfont=dict(color='white'),
1772
+ hovertemplate="<b>%{y}</b>: %{x:,}<extra></extra>"
1773
+ )
1774
+ fig_global.update_layout(
1775
+ **_dark_layout(
1776
+ xaxis=dict(title="", showticklabels=False, showgrid=False, zeroline=False),
1777
+ yaxis=dict(title="", showgrid=False, tickfont=dict(size=12, color="#c8d6e5")),
1778
+ margin=dict(l=0, r=20, t=10, b=0),
1779
+ height=160,
1780
+ )
1781
+ )
1782
+ st.plotly_chart(fig_global, use_container_width=True, key="global_status_bar", config={"displayModeBar": False})
1783
+
1784
+ with col_dac2:
1785
+ st.markdown("### 📡 Float Status Summary")
1786
 
1787
  # Dominant instrument per institution from meta registry
1788
  _inst_top_model = (
streamlit/plot_utils.py CHANGED
@@ -1,23 +1,39 @@
1
- import matplotlib.pyplot as plt
2
- import matplotlib.dates as mdates
3
  import numpy as np
4
  import xarray as xr
5
  import gsw
 
 
6
 
7
  def get_valid_data(ds_prof):
8
- # Flatten the data for scatter plots
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  cycles_2d = np.repeat(ds_prof.CYCLE_NUMBER.values[:, np.newaxis], ds_prof.PRES.shape[1], axis=1)
10
  dates_2d = np.repeat(ds_prof.JULD.values[:, np.newaxis], ds_prof.PRES.shape[1], axis=1)
11
 
12
- # We might have missing values in some profiles
13
  lon = ds_prof.LONGITUDE.values
14
  lat = ds_prof.LATITUDE.values
15
  lon_2d = np.repeat(lon[:, np.newaxis], ds_prof.PRES.shape[1], axis=1)
16
  lat_2d = np.repeat(lat[:, np.newaxis], ds_prof.PRES.shape[1], axis=1)
17
 
18
- pres = ds_prof.PRES.values.flatten()
19
- temp = ds_prof.TEMP.values.flatten() if 'TEMP' in ds_prof else np.full_like(pres, np.nan)
20
- psal = ds_prof.PSAL.values.flatten() if 'PSAL' in ds_prof else np.full_like(pres, np.nan)
 
21
  cycles = cycles_2d.flatten()
22
  dates = dates_2d.flatten()
23
  lon_flat = lon_2d.flatten()
@@ -33,54 +49,84 @@ def get_valid_data(ds_prof):
33
  lon_flat = lon_flat[valid]
34
  lat_flat = lat_flat[valid]
35
 
36
- # Compute Density (sigma0)
37
  SA = gsw.SA_from_SP(psal, pres, lon_flat, lat_flat)
38
  CT = gsw.CT_from_t(SA, temp, pres)
39
  rho = gsw.sigma0(SA, CT)
40
 
41
  return cycles, dates, pres, temp, psal, rho
42
 
43
- def create_ts_diagram(cycles, temp, psal, wmo):
44
- fig, ax = plt.subplots(figsize=(6, 5))
45
- sc = ax.scatter(psal, temp, c=cycles, cmap='jet', s=5, alpha=0.8)
46
- ax.set_xlabel("Practical Salinity (PSU)")
47
- ax.set_ylabel("Temperature (°C)")
48
- ax.set_title("T/S Diagram")
49
- cbar = plt.colorbar(sc, ax=ax)
50
- cbar.set_label("Profile number")
51
- fig.tight_layout()
52
- return fig
 
 
 
 
 
 
 
53
 
54
- def create_section_chart(dates, pres, z_var, z_label, title, wmo, cmap='jet'):
55
- fig, ax = plt.subplots(figsize=(6, 5))
56
- sc = ax.scatter(dates, pres, c=z_var, cmap=cmap, s=15, marker='s', edgecolors='none')
57
- ax.invert_yaxis()
58
- ax.set_ylabel("Pressure (dbar)")
59
- ax.set_title(title)
60
 
61
- # Format x-axis dates
62
- ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%Y'))
63
- plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha='right')
64
-
65
- cbar = plt.colorbar(sc, ax=ax)
66
- cbar.set_label(z_label)
67
- fig.tight_layout()
 
 
 
 
 
 
 
68
  return fig
69
 
70
- def create_overlaid_profiles(x_var, pres, cycles, x_label, title, wmo, cmap='jet'):
71
- fig, ax = plt.subplots(figsize=(6, 5))
72
 
73
- # Instead of lines which might look messy if flattened, scatter is fine,
74
- # but to draw lines we group by cycle. For performance and exact match to
75
- # the screenshot (which uses lines/scatter with color mapped to cycle),
76
- # a scatter plot with small points looks identical to dense overlaid lines.
77
- sc = ax.scatter(x_var, pres, c=cycles, cmap=cmap, s=2, alpha=0.8)
78
- ax.invert_yaxis()
79
- ax.set_xlabel(x_label)
80
- ax.set_ylabel("Pressure (dbar)")
81
- ax.set_title(title)
 
 
 
 
 
 
 
 
 
 
82
 
83
- cbar = plt.colorbar(sc, ax=ax)
84
- cbar.set_label("Profile number")
85
- fig.tight_layout()
 
 
 
 
 
 
 
 
 
 
 
86
  return fig
 
 
 
1
  import numpy as np
2
  import xarray as xr
3
  import gsw
4
+ import plotly.graph_objects as go
5
+ import pandas as pd
6
 
7
  def get_valid_data(ds_prof):
8
+ def get_var(name):
9
+ adj_name = f"{name}_ADJUSTED"
10
+ if adj_name in ds_prof:
11
+ val = ds_prof[adj_name].values.flatten()
12
+ if not np.isnan(val).all():
13
+ return val
14
+ if name in ds_prof:
15
+ return ds_prof[name].values.flatten()
16
+
17
+ # Fallback if variable doesn't exist
18
+ if 'PRES' in ds_prof:
19
+ return np.full_like(ds_prof.PRES.values.flatten(), np.nan)
20
+ return np.array([])
21
+
22
+ if 'CYCLE_NUMBER' not in ds_prof or 'PRES' not in ds_prof:
23
+ return np.array([]), np.array([]), np.array([]), np.array([]), np.array([]), np.array([])
24
+
25
  cycles_2d = np.repeat(ds_prof.CYCLE_NUMBER.values[:, np.newaxis], ds_prof.PRES.shape[1], axis=1)
26
  dates_2d = np.repeat(ds_prof.JULD.values[:, np.newaxis], ds_prof.PRES.shape[1], axis=1)
27
 
 
28
  lon = ds_prof.LONGITUDE.values
29
  lat = ds_prof.LATITUDE.values
30
  lon_2d = np.repeat(lon[:, np.newaxis], ds_prof.PRES.shape[1], axis=1)
31
  lat_2d = np.repeat(lat[:, np.newaxis], ds_prof.PRES.shape[1], axis=1)
32
 
33
+ pres = get_var('PRES')
34
+ temp = get_var('TEMP')
35
+ psal = get_var('PSAL')
36
+
37
  cycles = cycles_2d.flatten()
38
  dates = dates_2d.flatten()
39
  lon_flat = lon_2d.flatten()
 
49
  lon_flat = lon_flat[valid]
50
  lat_flat = lat_flat[valid]
51
 
 
52
  SA = gsw.SA_from_SP(psal, pres, lon_flat, lat_flat)
53
  CT = gsw.CT_from_t(SA, temp, pres)
54
  rho = gsw.sigma0(SA, CT)
55
 
56
  return cycles, dates, pres, temp, psal, rho
57
 
58
+ def _dark_layout(title, xlabel, ylabel, invert_y=False):
59
+ layout = dict(
60
+ title=title,
61
+ xaxis_title=xlabel,
62
+ yaxis_title=ylabel,
63
+ paper_bgcolor="rgba(0,0,0,0)",
64
+ plot_bgcolor="rgba(0,0,0,0)",
65
+ font=dict(family="Inter, sans-serif", color="#c8d6e5", size=12),
66
+ margin=dict(l=40, r=20, t=40, b=40),
67
+ xaxis=dict(gridcolor="rgba(255,255,255,0.1)", zerolinecolor="rgba(255,255,255,0.1)"),
68
+ yaxis=dict(gridcolor="rgba(255,255,255,0.1)", zerolinecolor="rgba(255,255,255,0.1)")
69
+ )
70
+ if xlabel == "Date":
71
+ layout["xaxis"]["tickformat"] = "%d/%m/%Y"
72
+ if invert_y:
73
+ layout["yaxis"]["autorange"] = "reversed"
74
+ return layout
75
 
76
+ def create_ts_diagram(cycles, temp, psal, wmo, title="T/S Diagram"):
77
+ fig = go.Figure()
 
 
 
 
78
 
79
+ fig.add_trace(go.Scattergl(
80
+ x=psal, y=temp,
81
+ mode='markers',
82
+ marker=dict(
83
+ size=4,
84
+ color=cycles,
85
+ colorscale='Jet',
86
+ showscale=True,
87
+ colorbar=dict(title="Profile<br>number")
88
+ ),
89
+ customdata=np.stack((cycles,), axis=-1),
90
+ hovertemplate="<b>Cycle:</b> %{customdata[0]}<br><b>Sal:</b> %{x:.3f} PSU<br><b>Temp:</b> %{y:.3f}°C<extra></extra>"
91
+ ))
92
+ fig.update_layout(**_dark_layout(title, "Practical Salinity (PSU)", "Temperature (°C)"))
93
  return fig
94
 
95
+ def create_section_chart(dates, pres, z_var, z_label, title, wmo, cmap='Jet'):
96
+ fig = go.Figure()
97
 
98
+ fig.add_trace(go.Scattergl(
99
+ x=dates, y=pres,
100
+ mode='markers',
101
+ marker=dict(
102
+ size=5,
103
+ symbol='square',
104
+ color=z_var,
105
+ colorscale=cmap,
106
+ showscale=True,
107
+ colorbar=dict(title=z_label.replace(' ', '<br>', 1))
108
+ ),
109
+ customdata=np.stack((z_var,), axis=-1),
110
+ hovertemplate="<b>Date:</b> %{x|%Y-%m-%d %H:%M}<br><b>Press:</b> %{y:.1f} dbar<br><b>" + z_label + ":</b> %{customdata[0]:.3f}<extra></extra>"
111
+ ))
112
+ fig.update_layout(**_dark_layout(title, "Date", "Pressure (dbar)", invert_y=True))
113
+ return fig
114
+
115
+ def create_overlaid_profiles(x_var, pres, cycles, x_label, title, wmo, cmap='Jet'):
116
+ fig = go.Figure()
117
 
118
+ fig.add_trace(go.Scattergl(
119
+ x=x_var, y=pres,
120
+ mode='markers',
121
+ marker=dict(
122
+ size=3,
123
+ color=cycles,
124
+ colorscale=cmap,
125
+ showscale=True,
126
+ colorbar=dict(title="Profile<br>number")
127
+ ),
128
+ customdata=np.stack((cycles,), axis=-1),
129
+ hovertemplate="<b>Cycle:</b> %{customdata[0]}<br><b>" + x_label + ":</b> %{x:.3f}<br><b>Press:</b> %{y:.1f} dbar<extra></extra>"
130
+ ))
131
+ fig.update_layout(**_dark_layout(title, x_label, "Pressure (dbar)", invert_y=True))
132
  return fig