Spaces:
Sleeping
Sleeping
harshini9942 commited on
Commit ·
78f8034
1
Parent(s): 0ebe8a8
dynamic plots
Browse files- streamlit/dashboard.py +312 -103
streamlit/dashboard.py
CHANGED
|
@@ -431,6 +431,70 @@ def _bgc_wmo_set(_df_bio):
|
|
| 431 |
return set(_df_bio["wmo_id"].dropna().unique())
|
| 432 |
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
# ==================== LOAD DATA ====================
|
| 435 |
with st.spinner("🌊 Initialising ARGO Dashboard …"):
|
| 436 |
df_prof = load_profile_data()
|
|
@@ -555,24 +619,42 @@ def show_float_details(wmo):
|
|
| 555 |
cycle_age = "N/A"
|
| 556 |
|
| 557 |
try:
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
if len(
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
else:
|
| 577 |
surf_data = "N/A"
|
| 578 |
bott_data = "N/A"
|
|
@@ -682,29 +764,29 @@ def show_float_details(wmo):
|
|
| 682 |
c1, c2, c3 = st.columns(3)
|
| 683 |
with c1:
|
| 684 |
fig = plot_utils.create_ts_diagram(cycles, temp, psal, wmo)
|
| 685 |
-
st.
|
| 686 |
with c2:
|
| 687 |
fig = plot_utils.create_section_chart(dates, pres, temp, "Temperature (°C)", "Section chart TEMP", wmo)
|
| 688 |
-
st.
|
| 689 |
with c3:
|
| 690 |
fig = plot_utils.create_section_chart(dates, pres, psal, "Salinity (PSU)", "Section chart PSAL", wmo)
|
| 691 |
-
st.
|
| 692 |
|
| 693 |
c4, c5, c6 = st.columns(3)
|
| 694 |
with c4:
|
| 695 |
fig = plot_utils.create_section_chart(dates, pres, rho, "Potential Density (kg/m³)", "Section chart RHO", wmo)
|
| 696 |
-
st.
|
| 697 |
with c5:
|
| 698 |
fig = plot_utils.create_overlaid_profiles(temp, pres, cycles, "Temperature (°C)", "Overlaid profiles TEMP", wmo)
|
| 699 |
-
st.
|
| 700 |
with c6:
|
| 701 |
fig = plot_utils.create_overlaid_profiles(psal, pres, cycles, "Salinity (PSU)", "Overlaid profiles PSAL", wmo)
|
| 702 |
-
st.
|
| 703 |
|
| 704 |
c7, c8, c9 = st.columns(3)
|
| 705 |
with c7:
|
| 706 |
fig = plot_utils.create_overlaid_profiles(rho, pres, cycles, "Potential Density (kg/m³)", "Overlaid profiles RHO", wmo)
|
| 707 |
-
st.
|
| 708 |
else:
|
| 709 |
st.info("No valid profile data available for technical plots.")
|
| 710 |
except Exception as e:
|
|
@@ -1529,9 +1611,9 @@ with col_fleet:
|
|
| 1529 |
st.markdown("---")
|
| 1530 |
col_dac1, col_dac2 = st.columns(2, gap="medium")
|
| 1531 |
|
| 1532 |
-
if len(
|
| 1533 |
dac_profs = (
|
| 1534 |
-
|
| 1535 |
.agg(Profiles=("file", "count"))
|
| 1536 |
.reset_index()
|
| 1537 |
)
|
|
@@ -1565,9 +1647,9 @@ if len(filt_prof) > 0:
|
|
| 1565 |
|
| 1566 |
with col_dac2:
|
| 1567 |
st.markdown("### 📡 Float Status Summary")
|
| 1568 |
-
latest_date =
|
| 1569 |
ninety_days_ago = pd.Timestamp(latest_date - timedelta(days=90))
|
| 1570 |
-
float_latest =
|
| 1571 |
float_latest["is_live"] = float_latest["date"] >= ninety_days_ago
|
| 1572 |
|
| 1573 |
live_df = float_latest.groupby("institution").agg(
|
|
@@ -1615,100 +1697,227 @@ else:
|
|
| 1615 |
st.info("No data available for summary tables.")
|
| 1616 |
|
| 1617 |
# ================================================================
|
| 1618 |
-
# ROW 4 — INCOIS Deployment Matrix (
|
| 1619 |
# ================================================================
|
| 1620 |
st.markdown("---")
|
| 1621 |
-
st.markdown("### 🗓️ INCOIS Float Deployments (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1622 |
|
| 1623 |
-
|
| 1624 |
-
|
| 1625 |
-
|
| 1626 |
-
|
| 1627 |
-
|
| 1628 |
-
|
| 1629 |
-
|
| 1630 |
-
|
| 1631 |
-
|
| 1632 |
-
|
| 1633 |
-
|
| 1634 |
-
|
| 1635 |
-
|
| 1636 |
-
|
| 1637 |
-
|
| 1638 |
-
|
| 1639 |
-
|
| 1640 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1641 |
pivot = merged.pivot_table(
|
| 1642 |
-
index="Month",
|
| 1643 |
-
|
| 1644 |
-
values="wmo_id",
|
| 1645 |
-
aggfunc="count",
|
| 1646 |
-
fill_value=0
|
| 1647 |
)
|
| 1648 |
-
|
| 1649 |
-
|
| 1650 |
-
|
| 1651 |
-
|
| 1652 |
-
|
| 1653 |
-
month_names = {
|
| 1654 |
-
1: "Jan", 2: "Feb", 3: "Mar", 4: "Apr", 5: "May", 6: "Jun",
|
| 1655 |
-
7: "Jul", 8: "Aug", 9: "Sep", 10: "Oct", 11: "Nov", 12: "Dec"
|
| 1656 |
}
|
| 1657 |
-
pivot.
|
| 1658 |
-
|
| 1659 |
-
# Calculate Row and Column Totals
|
| 1660 |
-
pivot["Total"] = pivot.sum(axis=1)
|
| 1661 |
pivot.loc["Total"] = pivot.sum(axis=0)
|
| 1662 |
-
|
| 1663 |
-
|
| 1664 |
-
|
| 1665 |
-
|
| 1666 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1667 |
st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
|
| 1668 |
-
|
| 1669 |
-
|
| 1670 |
-
|
| 1671 |
-
|
|
|
|
|
|
|
| 1672 |
body_html = ""
|
| 1673 |
-
for
|
| 1674 |
-
is_total_row =
|
| 1675 |
-
row_bg
|
| 1676 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1677 |
for col in pivot.columns:
|
| 1678 |
-
val
|
| 1679 |
-
|
| 1680 |
-
|
| 1681 |
-
|
| 1682 |
-
|
| 1683 |
-
|
| 1684 |
-
|
| 1685 |
-
|
| 1686 |
-
if col == "Pending" and val > 0:
|
| 1687 |
-
style += " color:#EF5350;"
|
| 1688 |
-
|
| 1689 |
-
row_html += f"<td style='{style}'>{val_str}</td>"
|
| 1690 |
body_html += f"<tr style='{row_bg}'>{row_html}</tr>"
|
| 1691 |
-
|
| 1692 |
st.markdown(
|
| 1693 |
-
f
|
| 1694 |
-
<div style="overflow-x:auto;
|
| 1695 |
-
|
| 1696 |
-
|
| 1697 |
-
|
| 1698 |
-
|
| 1699 |
-
|
|
|
|
|
|
|
| 1700 |
</table>
|
| 1701 |
</div>
|
| 1702 |
-
|
| 1703 |
-
unsafe_allow_html=True
|
| 1704 |
)
|
| 1705 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 1706 |
else:
|
| 1707 |
st.info("No INCOIS deployment data found.")
|
| 1708 |
else:
|
| 1709 |
st.info("No data available for deployment matrix.")
|
| 1710 |
-
|
| 1711 |
-
|
| 1712 |
# ================================================================
|
| 1713 |
# RAW DATA VIEWER (bonus — not in PRD but useful for ops)
|
| 1714 |
# ================================================================
|
|
|
|
| 431 |
return set(_df_bio["wmo_id"].dropna().unique())
|
| 432 |
|
| 433 |
|
| 434 |
+
# ==================== LAUNCH DATE HELPERS ====================
|
| 435 |
+
|
| 436 |
+
def _read_launch_date_from_nc(meta_path):
|
| 437 |
+
"""Read LAUNCH_DATE from a single float meta NetCDF. Returns 14-char string or None."""
|
| 438 |
+
try:
|
| 439 |
+
ds = xr.open_dataset(meta_path)
|
| 440 |
+
if "LAUNCH_DATE" not in ds:
|
| 441 |
+
ds.close()
|
| 442 |
+
return None
|
| 443 |
+
raw = ds.LAUNCH_DATE.values
|
| 444 |
+
ds.close()
|
| 445 |
+
if hasattr(raw, "item"):
|
| 446 |
+
try:
|
| 447 |
+
raw = raw.item()
|
| 448 |
+
except Exception:
|
| 449 |
+
pass
|
| 450 |
+
if isinstance(raw, bytes):
|
| 451 |
+
return raw.decode("utf-8", errors="ignore").strip()
|
| 452 |
+
return str(raw).strip()
|
| 453 |
+
except Exception:
|
| 454 |
+
return None
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def _load_launch_date_csv(launch_csv):
|
| 458 |
+
"""Load incois_launch_dates.csv, returning empty DataFrame on error."""
|
| 459 |
+
if not launch_csv.exists():
|
| 460 |
+
return pd.DataFrame(columns=["wmo_id", "launch_date"])
|
| 461 |
+
try:
|
| 462 |
+
df = pd.read_csv(launch_csv, dtype=str)
|
| 463 |
+
df["wmo_id"] = df["wmo_id"].str.strip()
|
| 464 |
+
return df
|
| 465 |
+
except Exception:
|
| 466 |
+
return pd.DataFrame(columns=["wmo_id", "launch_date"])
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def _scan_existing_nc_for_launch_dates(incois_wmo_set, launch_csv):
|
| 470 |
+
"""
|
| 471 |
+
Scan already-downloaded more_components/{wmo}_meta.nc files and extract
|
| 472 |
+
LAUNCH_DATE for any INCOIS float not yet in the CSV.
|
| 473 |
+
Returns count of NEW entries added.
|
| 474 |
+
"""
|
| 475 |
+
target_dir = BASE_DIR / "more_components"
|
| 476 |
+
if not target_dir.exists():
|
| 477 |
+
return 0
|
| 478 |
+
existing = _load_launch_date_csv(launch_csv)
|
| 479 |
+
already_have = set(existing["wmo_id"].tolist())
|
| 480 |
+
new_rows = []
|
| 481 |
+
for wmo in incois_wmo_set:
|
| 482 |
+
if wmo in already_have:
|
| 483 |
+
continue
|
| 484 |
+
meta_path = target_dir / f"{wmo}_meta.nc"
|
| 485 |
+
if not meta_path.exists():
|
| 486 |
+
continue
|
| 487 |
+
ld = _read_launch_date_from_nc(meta_path)
|
| 488 |
+
if ld and len(ld) >= 8:
|
| 489 |
+
new_rows.append({"wmo_id": wmo, "launch_date": ld})
|
| 490 |
+
if new_rows:
|
| 491 |
+
CACHE_DIR.mkdir(exist_ok=True)
|
| 492 |
+
updated = pd.concat([existing, pd.DataFrame(new_rows)], ignore_index=True)
|
| 493 |
+
updated = updated.drop_duplicates("wmo_id")
|
| 494 |
+
updated.to_csv(launch_csv, index=False)
|
| 495 |
+
return len(new_rows)
|
| 496 |
+
|
| 497 |
+
|
| 498 |
# ==================== LOAD DATA ====================
|
| 499 |
with st.spinner("🌊 Initialising ARGO Dashboard …"):
|
| 500 |
df_prof = load_profile_data()
|
|
|
|
| 619 |
cycle_age = "N/A"
|
| 620 |
|
| 621 |
try:
|
| 622 |
+
def get_ds_var(name):
|
| 623 |
+
adj_name = f"{name}_ADJUSTED"
|
| 624 |
+
if adj_name in ds_prof:
|
| 625 |
+
val = ds_prof[adj_name].values
|
| 626 |
+
if not np.isnan(val).all():
|
| 627 |
+
return val
|
| 628 |
+
if name in ds_prof:
|
| 629 |
+
return ds_prof[name].values
|
| 630 |
+
return None
|
| 631 |
+
|
| 632 |
+
pres_data = get_ds_var('PRES')
|
| 633 |
+
if pres_data is not None:
|
| 634 |
+
valid_cycles = np.where(~np.isnan(pres_data).all(axis=1))[0]
|
| 635 |
+
if len(valid_cycles) > 0:
|
| 636 |
+
last_valid_idx = valid_cycles[-1]
|
| 637 |
+
last_pres = pres_data[last_valid_idx]
|
| 638 |
+
|
| 639 |
+
temp_data = get_ds_var('TEMP')
|
| 640 |
+
last_temp = temp_data[last_valid_idx] if temp_data is not None else np.full_like(last_pres, np.nan)
|
| 641 |
+
|
| 642 |
+
psal_data = get_ds_var('PSAL')
|
| 643 |
+
last_psal = psal_data[last_valid_idx] if psal_data is not None else np.full_like(last_pres, np.nan)
|
| 644 |
+
|
| 645 |
+
valid_idx = ~np.isnan(last_pres)
|
| 646 |
+
pres_v = last_pres[valid_idx]
|
| 647 |
+
temp_v = last_temp[valid_idx]
|
| 648 |
+
psal_v = last_psal[valid_idx]
|
| 649 |
+
|
| 650 |
+
if len(pres_v) > 0:
|
| 651 |
+
surface_idx = np.argmin(pres_v)
|
| 652 |
+
bottom_idx = np.argmax(pres_v)
|
| 653 |
+
surf_data = f"{pres_v[surface_idx]:.2f} dbar {temp_v[surface_idx]:.3f}°C {psal_v[surface_idx]:.3f} PSU"
|
| 654 |
+
bott_data = f"{pres_v[bottom_idx]:.2f} dbar {temp_v[bottom_idx]:.3f}°C {psal_v[bottom_idx]:.3f} PSU"
|
| 655 |
+
else:
|
| 656 |
+
surf_data = "N/A"
|
| 657 |
+
bott_data = "N/A"
|
| 658 |
else:
|
| 659 |
surf_data = "N/A"
|
| 660 |
bott_data = "N/A"
|
|
|
|
| 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.")
|
| 792 |
except Exception as e:
|
|
|
|
| 1611 |
st.markdown("---")
|
| 1612 |
col_dac1, col_dac2 = st.columns(2, gap="medium")
|
| 1613 |
|
| 1614 |
+
if len(df_prof) > 0:
|
| 1615 |
dac_profs = (
|
| 1616 |
+
df_prof.groupby("institution")
|
| 1617 |
.agg(Profiles=("file", "count"))
|
| 1618 |
.reset_index()
|
| 1619 |
)
|
|
|
|
| 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(
|
|
|
|
| 1697 |
st.info("No data available for summary tables.")
|
| 1698 |
|
| 1699 |
# ================================================================
|
| 1700 |
+
# ROW 4 — INCOIS Deployment Matrix (Year vs Month)
|
| 1701 |
# ================================================================
|
| 1702 |
st.markdown("---")
|
| 1703 |
+
st.markdown("### 🗓️ INCOIS Float Deployments (Year vs Month)")
|
| 1704 |
+
|
| 1705 |
+
if len(df_meta) > 0:
|
| 1706 |
+
# --- All INCOIS floats from the authoritative metadata registry ---
|
| 1707 |
+
# FIX 1: Use both DAC and institution to catch all INCOIS floats
|
| 1708 |
+
meta_in = df_meta[
|
| 1709 |
+
(df_meta["dac"].str.lower().str.strip() == "incois") |
|
| 1710 |
+
(df_meta["institution"].str.upper().str.strip() == "IN")
|
| 1711 |
+
].drop_duplicates(subset=["wmo_id"]).copy()
|
| 1712 |
+
|
| 1713 |
+
meta_in["wmo_id"] = meta_in["wmo_id"].astype(str).str.strip()
|
| 1714 |
+
|
| 1715 |
+
if len(meta_in) > 0:
|
| 1716 |
+
launch_csv = CACHE_DIR / "incois_launch_dates.csv"
|
| 1717 |
+
incois_wmos = set(meta_in["wmo_id"].tolist())
|
| 1718 |
+
|
| 1719 |
+
# ------------------------------------------------------------------
|
| 1720 |
+
# STEP 1 — Silently absorb any already-downloaded meta NC files.
|
| 1721 |
+
# ------------------------------------------------------------------
|
| 1722 |
+
_scan_existing_nc_for_launch_dates(incois_wmos, launch_csv)
|
| 1723 |
+
|
| 1724 |
+
# ------------------------------------------------------------------
|
| 1725 |
+
# STEP 2 — Load the CSV cache
|
| 1726 |
+
# FIX 2: Drop duplicates to prevent overcounting in pivot table
|
| 1727 |
+
# ------------------------------------------------------------------
|
| 1728 |
+
ld_raw = _load_launch_date_csv(launch_csv).drop_duplicates(subset=["wmo_id"], keep="first")
|
| 1729 |
+
ld_raw["launch_date_parsed"] = pd.to_datetime(
|
| 1730 |
+
ld_raw["launch_date"], format="%Y%m%d%H%M%S", errors="coerce"
|
| 1731 |
+
)
|
| 1732 |
+
launch_dates = ld_raw[["wmo_id", "launch_date_parsed"]]
|
| 1733 |
+
|
| 1734 |
+
cached_wmos = set(launch_dates["wmo_id"].tolist())
|
| 1735 |
+
missing_wmos = sorted(incois_wmos - cached_wmos)
|
| 1736 |
+
|
| 1737 |
+
# ------------------------------------------------------------------
|
| 1738 |
+
# STEP 3 — Optional fetch button for floats whose NC files have
|
| 1739 |
+
# never been downloaded.
|
| 1740 |
+
# ------------------------------------------------------------------
|
| 1741 |
+
if missing_wmos:
|
| 1742 |
+
_dac_lookup = (
|
| 1743 |
+
df_meta[df_meta["wmo_id"].isin(missing_wmos)]
|
| 1744 |
+
.set_index("wmo_id")["dac"]
|
| 1745 |
+
.to_dict()
|
| 1746 |
+
)
|
| 1747 |
|
| 1748 |
+
with st.expander(
|
| 1749 |
+
f"⚠️ Launch dates missing for **{len(missing_wmos)}** floats — click to fetch from GDAC",
|
| 1750 |
+
expanded=False,
|
| 1751 |
+
):
|
| 1752 |
+
st.caption(
|
| 1753 |
+
"This fetches each float's `_meta.nc` from IFREMER GDAC over HTTPS and "
|
| 1754 |
+
"caches the `LAUNCH_DATE` field locally. Run once; results are saved to "
|
| 1755 |
+
f"`{launch_csv.name}` and reused on every subsequent load."
|
| 1756 |
+
)
|
| 1757 |
+
if st.button("🌐 Fetch Missing Launch Dates from GDAC", key="fetch_launch_dates"):
|
| 1758 |
+
import urllib.request as _urlreq
|
| 1759 |
+
|
| 1760 |
+
target_dir = BASE_DIR / "more_components"
|
| 1761 |
+
target_dir.mkdir(exist_ok=True)
|
| 1762 |
+
|
| 1763 |
+
existing_csv = _load_launch_date_csv(launch_csv)
|
| 1764 |
+
new_rows = []
|
| 1765 |
+
failed = []
|
| 1766 |
+
prog = st.progress(0.0)
|
| 1767 |
+
status_ph = st.empty()
|
| 1768 |
+
total = len(missing_wmos)
|
| 1769 |
+
|
| 1770 |
+
for idx, wmo in enumerate(missing_wmos, 1):
|
| 1771 |
+
status_ph.markdown(f"Fetching **{wmo}** ({idx}/{total})…")
|
| 1772 |
+
prog.progress(idx / total)
|
| 1773 |
+
|
| 1774 |
+
meta_path = target_dir / f"{wmo}_meta.nc"
|
| 1775 |
+
dac = _dac_lookup.get(wmo, "incois")
|
| 1776 |
+
|
| 1777 |
+
if not meta_path.exists():
|
| 1778 |
+
url = f"https://data-argo.ifremer.fr/dac/{dac}/{wmo}/{wmo}_meta.nc"
|
| 1779 |
+
try:
|
| 1780 |
+
_urlreq.urlretrieve(url, meta_path)
|
| 1781 |
+
except Exception as e:
|
| 1782 |
+
failed.append((wmo, str(e)))
|
| 1783 |
+
continue
|
| 1784 |
+
|
| 1785 |
+
ld = _read_launch_date_from_nc(meta_path)
|
| 1786 |
+
if ld and len(ld) >= 8:
|
| 1787 |
+
new_rows.append({"wmo_id": wmo, "launch_date": ld})
|
| 1788 |
+
else:
|
| 1789 |
+
failed.append((wmo, "LAUNCH_DATE not found in NetCDF"))
|
| 1790 |
+
|
| 1791 |
+
prog.empty()
|
| 1792 |
+
status_ph.empty()
|
| 1793 |
+
|
| 1794 |
+
if new_rows:
|
| 1795 |
+
CACHE_DIR.mkdir(exist_ok=True)
|
| 1796 |
+
updated = pd.concat(
|
| 1797 |
+
[existing_csv, pd.DataFrame(new_rows)], ignore_index=True
|
| 1798 |
+
).drop_duplicates("wmo_id")
|
| 1799 |
+
updated.to_csv(launch_csv, index=False)
|
| 1800 |
+
st.success(f"��� Cached launch dates for {len(new_rows)} floats. {len(failed)} could not be fetched.")
|
| 1801 |
+
st.rerun()
|
| 1802 |
+
else:
|
| 1803 |
+
st.error(f"Could not fetch any new launch dates. {len(failed)} failures.")
|
| 1804 |
+
|
| 1805 |
+
# ------------------------------------------------------------------
|
| 1806 |
+
# STEP 4 — Determine deployment date for every INCOIS float.
|
| 1807 |
+
# ------------------------------------------------------------------
|
| 1808 |
+
earliest_profile = (
|
| 1809 |
+
df_prof[df_prof["wmo_id"].isin(incois_wmos)]
|
| 1810 |
+
.groupby("wmo_id")["date"]
|
| 1811 |
+
.min()
|
| 1812 |
+
.reset_index()
|
| 1813 |
+
.rename(columns={"date": "earliest_profile_date"})
|
| 1814 |
+
)
|
| 1815 |
+
earliest_profile["wmo_id"] = earliest_profile["wmo_id"].astype(str).str.strip()
|
| 1816 |
+
|
| 1817 |
+
merged = meta_in[["wmo_id"]].copy()
|
| 1818 |
+
merged = pd.merge(merged, launch_dates, on="wmo_id", how="left")
|
| 1819 |
+
merged = pd.merge(merged, earliest_profile, on="wmo_id", how="left")
|
| 1820 |
+
merged["deploy_date"] = merged["launch_date_parsed"].fillna(merged["earliest_profile_date"])
|
| 1821 |
+
|
| 1822 |
+
n_true = int(merged["launch_date_parsed"].notna().sum())
|
| 1823 |
+
n_proxy = int((merged["launch_date_parsed"].isna() & merged["earliest_profile_date"].notna()).sum())
|
| 1824 |
+
n_unknown = int(merged["deploy_date"].isna().sum())
|
| 1825 |
+
|
| 1826 |
+
merged = merged.dropna(subset=["deploy_date"])
|
| 1827 |
+
merged["Year"] = merged["deploy_date"].dt.year.astype(int)
|
| 1828 |
+
merged["Month"] = merged["deploy_date"].dt.month.astype(int)
|
| 1829 |
+
|
| 1830 |
+
# ------------------------------------------------------------------
|
| 1831 |
+
# STEP 5 — Pivot: one row per year, one column per month.
|
| 1832 |
+
# ------------------------------------------------------------------
|
| 1833 |
pivot = merged.pivot_table(
|
| 1834 |
+
index="Year", columns="Month", values="wmo_id",
|
| 1835 |
+
aggfunc="count", fill_value=0,
|
|
|
|
|
|
|
|
|
|
| 1836 |
)
|
| 1837 |
+
pivot = pivot.reindex(columns=range(1, 13), fill_value=0)
|
| 1838 |
+
|
| 1839 |
+
MONTH_NAMES = {
|
| 1840 |
+
1:"JAN", 2:"FEB", 3:"MAR", 4:"APR", 5:"MAY", 6:"JUN",
|
| 1841 |
+
7:"JUL", 8:"AUG", 9:"SEP", 10:"OCT", 11:"NOV", 12:"DEC",
|
|
|
|
|
|
|
|
|
|
| 1842 |
}
|
| 1843 |
+
pivot.columns = [MONTH_NAMES[m] for m in pivot.columns]
|
| 1844 |
+
pivot["Total"] = pivot.sum(axis=1)
|
|
|
|
|
|
|
| 1845 |
pivot.loc["Total"] = pivot.sum(axis=0)
|
| 1846 |
+
total_floats = int(pivot.loc["Total", "Total"])
|
| 1847 |
+
|
| 1848 |
+
# ------------------------------------------------------------------
|
| 1849 |
+
# STEP 6 — Render
|
| 1850 |
+
# ------------------------------------------------------------------
|
| 1851 |
+
badge_parts = [
|
| 1852 |
+
f"<span style='color:#8BC34A'>✓ {n_true} true launch dates</span>",
|
| 1853 |
+
f"<span style='color:#FFB74D'>~ {n_proxy} profile-date proxy</span>",
|
| 1854 |
+
]
|
| 1855 |
+
if n_unknown:
|
| 1856 |
+
badge_parts.append(f"<span style='color:#EF5350'>✗ {n_unknown} unknown (excluded)</span>")
|
| 1857 |
+
|
| 1858 |
+
proxy_pct = round(100 * n_proxy / max(n_true + n_proxy, 1))
|
| 1859 |
+
if proxy_pct > 20 and n_unknown > 0:
|
| 1860 |
+
st.warning(f"⚠️ {n_unknown} floats have no date source. Click the **Fetch Missing Launch Dates** expander above to fix this.")
|
| 1861 |
+
|
| 1862 |
+
st.markdown(
|
| 1863 |
+
f"<p style='color:#c8d6e5; font-size:1rem; margin-bottom:6px;'>"
|
| 1864 |
+
f"Total Deployed INCOIS Floats: "
|
| 1865 |
+
f"<strong style='color:#00BCD4; font-size:1.2rem;'>{total_floats:,}</strong>"
|
| 1866 |
+
f" · <span style='font-size:0.8rem;'>"
|
| 1867 |
+
+ " | ".join(badge_parts)
|
| 1868 |
+
+ "</span></p>",
|
| 1869 |
+
unsafe_allow_html=True,
|
| 1870 |
+
)
|
| 1871 |
+
|
| 1872 |
st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
|
| 1873 |
+
|
| 1874 |
+
header_html = (
|
| 1875 |
+
"<th style='position:sticky;left:0;background:#060b19;z-index:2;min-width:60px;'>Year</th>"
|
| 1876 |
+
+ "".join(f"<th style='min-width:48px;'>{m}</th>" for m in pivot.columns)
|
| 1877 |
+
)
|
| 1878 |
+
|
| 1879 |
body_html = ""
|
| 1880 |
+
for year in pivot.index:
|
| 1881 |
+
is_total_row = year == "Total"
|
| 1882 |
+
row_bg = "background:rgba(0,188,212,0.06);" if is_total_row else ""
|
| 1883 |
+
yr_lbl = "Total" if is_total_row else int(year)
|
| 1884 |
+
yr_bg = "#0c1427" if is_total_row else "#060b19"
|
| 1885 |
+
|
| 1886 |
+
row_html = (
|
| 1887 |
+
f"<td style='position:sticky;left:0;background:{yr_bg};"
|
| 1888 |
+
f"z-index:1;font-weight:bold;color:#00BCD4;'>{yr_lbl}</td>"
|
| 1889 |
+
)
|
| 1890 |
for col in pivot.columns:
|
| 1891 |
+
val = pivot.loc[year, col]
|
| 1892 |
+
is_tot = is_total_row or col == "Total"
|
| 1893 |
+
style = "font-weight:bold;color:#FFB74D;" if is_tot else ""
|
| 1894 |
+
cell = (
|
| 1895 |
+
f"{int(val):,}" if val > 0
|
| 1896 |
+
else "<span style='color:rgba(255,255,255,0.18)'>-</span>"
|
| 1897 |
+
)
|
| 1898 |
+
row_html += f"<td style='{style}'>{cell}</td>"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1899 |
body_html += f"<tr style='{row_bg}'>{row_html}</tr>"
|
| 1900 |
+
|
| 1901 |
st.markdown(
|
| 1902 |
+
f"""
|
| 1903 |
+
<div style="overflow-x:auto;max-height:600px;overflow-y:auto;
|
| 1904 |
+
border:1px solid rgba(255,255,255,0.1);border-radius:8px;">
|
| 1905 |
+
<table class="dac-table"
|
| 1906 |
+
style="width:100%;text-align:center;border-collapse:collapse;">
|
| 1907 |
+
<thead style='position:sticky;top:0;background:#060b19;z-index:3;'>
|
| 1908 |
+
<tr>{header_html}</tr>
|
| 1909 |
+
</thead>
|
| 1910 |
+
<tbody>{body_html}</tbody>
|
| 1911 |
</table>
|
| 1912 |
</div>
|
| 1913 |
+
""",
|
| 1914 |
+
unsafe_allow_html=True,
|
| 1915 |
)
|
| 1916 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 1917 |
else:
|
| 1918 |
st.info("No INCOIS deployment data found.")
|
| 1919 |
else:
|
| 1920 |
st.info("No data available for deployment matrix.")
|
|
|
|
|
|
|
| 1921 |
# ================================================================
|
| 1922 |
# RAW DATA VIEWER (bonus — not in PRD but useful for ops)
|
| 1923 |
# ================================================================
|