harshini9942 commited on
Commit
78f8034
·
1 Parent(s): 0ebe8a8

dynamic plots

Browse files
Files changed (1) hide show
  1. 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
- pres_data = ds_prof.PRES.values
559
- valid_cycles = np.where(~np.isnan(pres_data).all(axis=1))[0]
560
- if len(valid_cycles) > 0:
561
- last_valid_idx = valid_cycles[-1]
562
- last_pres = pres_data[last_valid_idx]
563
- last_temp = ds_prof.TEMP.values[last_valid_idx] if 'TEMP' in ds_prof else np.full_like(last_pres, np.nan)
564
- last_psal = ds_prof.PSAL.values[last_valid_idx] if 'PSAL' in ds_prof else np.full_like(last_pres, np.nan)
565
-
566
- valid_idx = ~np.isnan(last_pres)
567
- pres_v = last_pres[valid_idx]
568
- temp_v = last_temp[valid_idx]
569
- psal_v = last_psal[valid_idx]
570
-
571
- if len(pres_v) > 0:
572
- surface_idx = np.argmin(pres_v)
573
- bottom_idx = np.argmax(pres_v)
574
- surf_data = f"{pres_v[surface_idx]:.2f} dbar {temp_v[surface_idx]:.3f}°C {psal_v[surface_idx]:.3f} PSU"
575
- bott_data = f"{pres_v[bottom_idx]:.2f} dbar {temp_v[bottom_idx]:.3f}°C {psal_v[bottom_idx]:.3f} PSU"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.pyplot(fig, clear_figure=True)
686
  with c2:
687
  fig = plot_utils.create_section_chart(dates, pres, temp, "Temperature (°C)", "Section chart TEMP", wmo)
688
- st.pyplot(fig, clear_figure=True)
689
  with c3:
690
  fig = plot_utils.create_section_chart(dates, pres, psal, "Salinity (PSU)", "Section chart PSAL", wmo)
691
- st.pyplot(fig, clear_figure=True)
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.pyplot(fig, clear_figure=True)
697
  with c5:
698
  fig = plot_utils.create_overlaid_profiles(temp, pres, cycles, "Temperature (°C)", "Overlaid profiles TEMP", wmo)
699
- st.pyplot(fig, clear_figure=True)
700
  with c6:
701
  fig = plot_utils.create_overlaid_profiles(psal, pres, cycles, "Salinity (PSU)", "Overlaid profiles PSAL", wmo)
702
- st.pyplot(fig, clear_figure=True)
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.pyplot(fig, clear_figure=True)
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(filt_prof) > 0:
1533
  dac_profs = (
1534
- filt_prof.groupby("institution")
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 = filt_prof["date"].max()
1569
  ninety_days_ago = pd.Timestamp(latest_date - timedelta(days=90))
1570
- float_latest = filt_prof.dropna(subset=["date"]).groupby(["institution", "wmo_id"])["date"].max().reset_index()
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 (Month vs Year)
1619
  # ================================================================
1620
  st.markdown("---")
1621
- st.markdown("### 🗓️ INCOIS Float Deployments (Month vs Year)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1622
 
1623
- if len(df_prof) > 0:
1624
- incois_df = df_prof[df_prof["institution"] == "IN"]
1625
- if len(incois_df) > 0:
1626
- # Find the deployment date (earliest profile date per float)
1627
- deployments = incois_df.groupby("wmo_id")["date"].min().reset_index()
1628
-
1629
- # Merge with all registered INCOIS floats to capture ones that haven't profiled
1630
- meta_in = df_meta[df_meta["institution"] == "IN"].copy()
1631
- merged = pd.merge(meta_in, deployments, on="wmo_id", how="left")
1632
-
1633
- # If float hasn't profiled, fallback to metadata registration date (date_update)
1634
- merged["date_update"] = pd.to_datetime(merged["date_update"], format="%Y%m%d%H%M%S", errors='coerce')
1635
- merged["date_final"] = merged["date"].fillna(merged["date_update"])
1636
-
1637
- merged["Year"] = merged["date_final"].dt.year
1638
- merged["Month"] = merged["date_final"].dt.month
1639
-
1640
- # Create a pivot table: Months as rows, Years as columns
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1641
  pivot = merged.pivot_table(
1642
- index="Month",
1643
- columns="Year",
1644
- values="wmo_id",
1645
- aggfunc="count",
1646
- fill_value=0
1647
  )
1648
-
1649
- # Ensure all 12 months are displayed
1650
- all_months = range(1, 13)
1651
- pivot = pivot.reindex(all_months, fill_value=0)
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.index = pivot.index.map(month_names)
1658
-
1659
- # Calculate Row and Column Totals
1660
- pivot["Total"] = pivot.sum(axis=1)
1661
  pivot.loc["Total"] = pivot.sum(axis=0)
1662
-
1663
- total_in_floats = int(pivot.loc["Total", "Total"])
1664
-
1665
- st.markdown(f"<p style='color: #c8d6e5; font-size: 1rem;'>Total INCOIS Floats Registered: <strong style='color: #00BCD4; font-size: 1.2rem;'>{total_in_floats:,}</strong></p>", unsafe_allow_html=True)
1666
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1667
  st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1668
-
1669
- # Build HTML for the table
1670
- header_html = "<th>Month</th>" + "".join([f"<th>{y if isinstance(y, str) else int(y)}</th>" for y in pivot.columns])
1671
-
 
 
1672
  body_html = ""
1673
- for month in pivot.index:
1674
- is_total_row = (month == "Total")
1675
- row_bg = "background: rgba(0,188,212,0.06);" if is_total_row else ""
1676
- row_html = f"<td style='font-weight:bold; color:#00BCD4;'>{month}</td>"
 
 
 
 
 
 
1677
  for col in pivot.columns:
1678
- val = pivot.loc[month, col]
1679
- val_str = f"{int(val):,}" if val > 0 else "<span style='color:rgba(255,255,255,0.2)'>-</span>"
1680
-
1681
- is_total_col = (col == "Total")
1682
- style = ""
1683
- if is_total_row or is_total_col:
1684
- style = "font-weight:bold; color:#FFB74D;"
1685
- # Highlight Pending column slightly
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
- <table class="dac-table" style="width:100%; text-align:center;">
1696
- <thead><tr>{header_html}</tr></thead>
1697
- <tbody>
1698
- {body_html}
1699
- </tbody>
 
 
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}** &nbsp;({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" &nbsp;·&nbsp; <span style='font-size:0.8rem;'>"
1867
+ + " &nbsp;|&nbsp; ".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
  # ================================================================