Spaces:
Sleeping
Sleeping
harshini9942 commited on
Commit ·
231c749
1
Parent(s): 78f8034
streamlit changes
Browse files- streamlit/dashboard.py +148 -24
- 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1649 |
-
st.markdown("###
|
| 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 |
-
|
| 1656 |
-
|
| 1657 |
-
|
|
|
|
|
|
|
| 1658 |
|
| 1659 |
-
|
| 1660 |
-
|
| 1661 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 19 |
-
temp =
|
| 20 |
-
psal =
|
|
|
|
| 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
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
def
|
| 55 |
-
fig
|
| 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 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
return fig
|
| 69 |
|
| 70 |
-
def
|
| 71 |
-
fig
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|