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

Streamlit Dashboard Pushed 20-05-2026

Browse files
Files changed (47) hide show
  1. streamlit/.gitignore +1 -0
  2. streamlit/.streamlit/config.toml +11 -0
  3. streamlit/2902174_meta_test.nc +3 -0
  4. streamlit/ARGO_Dashboard_PRD.docx +0 -0
  5. streamlit/__pycache__/plot_utils.cpython-314.pyc +0 -0
  6. streamlit/analyze_data.py +85 -0
  7. streamlit/cache/bgc_profiles.parquet +3 -0
  8. streamlit/cache/meta.parquet +3 -0
  9. streamlit/cache/profiles.parquet +3 -0
  10. streamlit/dashboard.py +1772 -0
  11. streamlit/dashboard_example.py +360 -0
  12. streamlit/find_core_logic.py +43 -0
  13. streamlit/find_correct_logic.py +82 -0
  14. streamlit/hello.py +151 -0
  15. streamlit/more_components/2900552_meta.nc +3 -0
  16. streamlit/more_components/2900552_prof.nc +3 -0
  17. streamlit/more_components/2902174_meta.nc +3 -0
  18. streamlit/more_components/2902174_prof.nc +3 -0
  19. streamlit/more_components/2902771_meta.nc +3 -0
  20. streamlit/more_components/2902771_prof.nc +3 -0
  21. streamlit/more_components/2902821_meta.nc +3 -0
  22. streamlit/more_components/2902821_prof.nc +3 -0
  23. streamlit/more_components/2903145_meta.nc +3 -0
  24. streamlit/more_components/2903145_prof.nc +3 -0
  25. streamlit/more_components/2903424_meta.nc +3 -0
  26. streamlit/more_components/2903424_prof.nc +3 -0
  27. streamlit/more_components/5907180_meta.nc +3 -0
  28. streamlit/more_components/5907180_prof.nc +3 -0
  29. streamlit/more_components/7902408_meta.nc +3 -0
  30. streamlit/more_components/7902408_prof.nc +3 -0
  31. streamlit/plot_utils.py +86 -0
  32. streamlit/scratch/analyze_meta.py +46 -0
  33. streamlit/scratch/check_coordinates.py +34 -0
  34. streamlit/scratch/investigate_incois_floats.py +40 -0
  35. streamlit/scratch/test_land_mask.py +28 -0
  36. streamlit/scratch/test_pivot_dates.py +40 -0
  37. streamlit/scratch/verify_filter.py +41 -0
  38. streamlit/scratch_ftp_test.py +14 -0
  39. streamlit/scratch_nc_inspect.py +20 -0
  40. streamlit/scratch_nc_parse.py +53 -0
  41. streamlit/scratch_nc_vars.py +17 -0
  42. streamlit/scratch_nc_vars_2903424.py +29 -0
  43. streamlit/scratch_nc_vars_2903424_specific.py +22 -0
  44. streamlit/scratch_plot_test.py +10 -0
  45. streamlit/scratch_prof_test.py +47 -0
  46. streamlit/test_counts.py +20 -0
  47. streamlit/test_last7.py +21 -0
streamlit/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ *.txt
streamlit/.streamlit/config.toml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor = "#00BCD4"
3
+ backgroundColor = "#0a0e27"
4
+ secondaryBackgroundColor = "#111a38"
5
+ textColor = "#c8d6e5"
6
+ font = "sans serif"
7
+
8
+ [server]
9
+ headless = true
10
+ port = 8501
11
+ maxUploadSize = 500
streamlit/2902174_meta_test.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:237a8905395766789208be861c7b115998fd691ded7f00d0c12bda2dd5535934
3
+ size 68748
streamlit/ARGO_Dashboard_PRD.docx ADDED
Binary file (25.4 kB). View file
 
streamlit/__pycache__/plot_utils.cpython-314.pyc ADDED
Binary file (5.78 kB). View file
 
streamlit/analyze_data.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Quick analysis: how many floats survive each filtering stage?"""
2
+ import pandas as pd
3
+ from global_land_mask import globe
4
+ import numpy as np
5
+
6
+ print("=== Loading raw data ===")
7
+ df = pd.read_csv("ar_index_global_prof.txt", comment="#")
8
+ df.columns = df.columns.str.strip()
9
+ df["wmo_id"] = df["file"].str.extract(r"/(\d+)/")
10
+
11
+ print(f"1. Raw rows: {len(df):,}")
12
+ print(f" Raw unique floats: {df['wmo_id'].nunique():,}")
13
+
14
+ # After dropping NaN lat/lon
15
+ df2 = df.dropna(subset=["latitude", "longitude"])
16
+ print(f"\n2. After dropping NaN coords: {len(df2):,} rows, {df2['wmo_id'].nunique():,} floats")
17
+
18
+ # After valid range filter
19
+ df3 = df2[
20
+ (df2["latitude"] >= -90) & (df2["latitude"] <= 90)
21
+ & (df2["longitude"] >= -180) & (df2["longitude"] <= 180)
22
+ ]
23
+ print(f"3. After valid range: {len(df3):,} rows, {df3['wmo_id'].nunique():,} floats")
24
+
25
+ # After land masking
26
+ is_land = globe.is_land(df3["latitude"].values, df3["longitude"].values)
27
+ land_count = int(is_land.sum())
28
+ df4 = df3[~is_land]
29
+ print(f"4. Land-masked removed: {land_count:,} rows")
30
+ print(f" After land mask: {len(df4):,} rows, {df4['wmo_id'].nunique():,} floats")
31
+
32
+ # After Indian Ocean bounding box (default filters)
33
+ df5 = df4[
34
+ (df4["longitude"] >= 20.0) & (df4["longitude"] <= 145.0)
35
+ & (df4["latitude"] >= -70.1) & (df4["latitude"] <= 30.0)
36
+ ]
37
+ print(f"\n5. Indian Ocean box (20-145E, 70.1S-30N):")
38
+ print(f" Rows: {len(df5):,}, Unique floats: {df5['wmo_id'].nunique():,}")
39
+
40
+ # Latest position per float (what map shows)
41
+ df5_sorted = df5.copy()
42
+ df5_sorted["date"] = pd.to_datetime(df5_sorted["date"], format="%Y%m%d%H%M%S", errors="coerce")
43
+ map_df = (
44
+ df5_sorted.dropna(subset=["latitude", "longitude"])
45
+ .sort_values("date")
46
+ .groupby("wmo_id")
47
+ .tail(1)
48
+ )
49
+ print(f" Map markers (latest pos per float): {len(map_df):,}")
50
+
51
+ # Check institutions in Indian Ocean
52
+ print(f"\n6. Institutions in Indian Ocean box:")
53
+ inst_counts = df5.groupby("institution")["wmo_id"].nunique().sort_values(ascending=False)
54
+ for inst, count in inst_counts.items():
55
+ print(f" {inst}: {count:,} floats")
56
+ print(f" TOTAL: {inst_counts.sum():,}")
57
+
58
+ # Also check: how many floats does the GLOBAL dataset have whose LATEST position is in Indian Ocean?
59
+ print("\n7. Floats whose LATEST position falls in Indian Ocean:")
60
+ df4["date"] = pd.to_datetime(df4["date"], format="%Y%m%d%H%M%S", errors="coerce")
61
+ latest_pos = df4.dropna(subset=["date"]).sort_values("date").groupby("wmo_id").tail(1)
62
+ io_latest = latest_pos[
63
+ (latest_pos["longitude"] >= 20.0) & (latest_pos["longitude"] <= 145.0)
64
+ & (latest_pos["latitude"] >= -70.1) & (latest_pos["latitude"] <= 30.0)
65
+ ]
66
+ print(f" Floats with latest pos in IO: {len(io_latest):,}")
67
+
68
+ # Check: floats that EVER reported from Indian Ocean
69
+ print("\n8. Floats that EVER reported from Indian Ocean box:")
70
+ io_ever = df4[
71
+ (df4["longitude"] >= 20.0) & (df4["longitude"] <= 145.0)
72
+ & (df4["latitude"] >= -70.1) & (df4["latitude"] <= 30.0)
73
+ ]
74
+ print(f" Unique floats ever in IO: {io_ever['wmo_id'].nunique():,}")
75
+ print(f" Total profiles in IO: {len(io_ever):,}")
76
+
77
+ # Argo reference numbers
78
+ print("\n=== For reference ===")
79
+ print(f"Global active floats (latest profile in last 30 days):")
80
+ latest_date = df4["date"].max()
81
+ thirty_days = pd.Timestamp(latest_date - pd.Timedelta(days=30))
82
+ active_global = latest_pos[latest_pos["date"] >= thirty_days]
83
+ print(f" Global active: {len(active_global):,}")
84
+ active_io = io_latest[io_latest["date"] >= thirty_days]
85
+ print(f" Indian Ocean active: {len(active_io):,}")
streamlit/cache/bgc_profiles.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:db2cdd7770e0a93233a603d7b08a8a30be3e8d94c082dce046a30c6d02e24494
3
+ size 10149287
streamlit/cache/meta.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:99cee387efe37a0f64538c5da2a9d41bbeb9fc424b65760b5c60981198ca9d78
3
+ size 518271
streamlit/cache/profiles.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec9b42bd80e8c5c0d231fb0fdf3192670f75b2d948dd29b9114528e1f3f90a49
3
+ size 82055253
streamlit/dashboard.py ADDED
@@ -0,0 +1,1772 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Indian ARGO CTD / BGC Float Dashboard
3
+ ======================================
4
+ Streamlit re-implementation per INCOIS PRD.
5
+ Data Source: Argo GDAC (IFREMER)
6
+
7
+ Components
8
+ ----------
9
+ 1. Geospatial float-position map (colour-coded by institution/region)
10
+ 2. Annual float-count bar chart (1999–present)
11
+ 3. BGC profile KPI tiles (DOXY, Chla, Nitrate, pH)
12
+ 4. Active floats/profiles last-7-days treemap
13
+ 5. Float-age donut chart
14
+ 6. DAC/Institution summary table
15
+ """
16
+
17
+ # ==================== IMPORTS ====================
18
+ import streamlit as st
19
+ import os
20
+ import pandas as pd
21
+ import numpy as np
22
+ import plotly.express as px
23
+ import plotly.graph_objects as go
24
+ from datetime import datetime, timedelta
25
+ from pathlib import Path
26
+ import warnings
27
+ import xarray as xr
28
+
29
+ warnings.filterwarnings("ignore")
30
+
31
+ # ==================== PATHS & CONSTANTS ====================
32
+ BASE_DIR = Path(__file__).parent
33
+ CACHE_DIR = BASE_DIR / "cache"
34
+ PROF_FILE = BASE_DIR / "ar_index_global_prof.txt"
35
+ BIO_FILE = BASE_DIR / "argo_bio-profile_index.txt"
36
+ META_FILE = BASE_DIR / "ar_index_global_meta.txt"
37
+
38
+ # Institution → colour (PRD §7.1.2 / Table 6)
39
+ REGION_COLORS = {
40
+ "IN": "#8BC34A", # Indian Ocean – olive green
41
+ "AO": "#00BCD4", # Arabian / Atlantic Ocean – cyan
42
+ "BO": "#FF5722", # Bay of Bengal – deep orange
43
+ "CS": "#FFC107", # Coral Sea – amber
44
+ "HZ": "#9E9E9E", # Marginal seas – grey
45
+ "IF": "#4CAF50", # Intermediate / Far seas – green
46
+ "JA": "#2196F3", # JMA – blue
47
+ "KO": "#E91E63", # KIOST – pink
48
+ "KM": "#9C27B0", # KMA – purple
49
+ "ME": "#795548", # MEDS – brown
50
+ "NM": "#607D8B", # NMDIS – blue-grey
51
+ }
52
+
53
+
54
+
55
+ # KPI tile colours (PRD §7.3.2 / Table 7)
56
+ KPI_COLORS = {
57
+ "DOXY": "#00BCD4",
58
+ "Chla": "#8BC34A",
59
+ "Nitrate": "#FF8F00",
60
+ "pH": "#4CAF50",
61
+ }
62
+
63
+ # Age-group colours (PRD §7.5.2)
64
+ AGE_COLORS = {
65
+ "00-02": "#673AB7",
66
+ "03-05": "#2196F3",
67
+ "06-08": "#FF9800",
68
+ "09-11": "#F44336",
69
+ "12+": "#795548",
70
+ }
71
+
72
+ # Profiler Type (WMO R08 Table) → Human-readable instrument name
73
+ PROFILER_TYPE_NAMES = {
74
+ 831: "P-ALACE", 834: "Provor-II", 835: "Provor-III", 836: "Provor-MT",
75
+ 837: "Arvor-C", 838: "Arvor-D", 839: "Provor-IV", 840: "Provor (no CT)",
76
+ 841: "Provor-SBE", 842: "Arvor-CM", 843: "Provor-V", 844: "Arvor",
77
+ 845: "Webb-PALACE", 846: "APEX", 847: "APEX-EM", 848: "APEX-EM-SBE",
78
+ 849: "APEX-Deep", 850: "SOLO (no CT)", 851: "SOLO-SBE", 852: "SOLO-FSI",
79
+ 853: "SOLO2", 854: "S2A", 855: "Ninja (no CT)", 856: "Ninja-D",
80
+ 857: "Ninja-BGC", 858: "Ninja-Deep", 859: "Ninja-SBE", 860: "Ninja",
81
+ 861: "ALTO", 862: "Navis-EBR", 863: "Navis-A", 864: "Navis-Deep",
82
+ 865: "Nova", 869: "Deep ARVOR", 870: "APEX-APF11", 871: "APEX-Deep-APF11",
83
+ 872: "APEX-BGC", 873: "Arvor-Deep", 874: "APEX-Deep-SBE",
84
+ 875: "Provor-BGC", 876: "Deep SOLO", 877: "Deep SOLO-MRV",
85
+ 878: "Deep NINJA", 879: "HM2000", 880: "HM4000", 881: "Deep Arvor-O",
86
+ 882: "Deep S2A", 883: "Provor-BGC-II", 884: "Arvor-I", 885: "TWR",
87
+ 886: "SOLO-BGC", 887: "Arvor-RBR", 888: "ALTO-RBR", 889: "Arvor-Deep-RBR",
88
+ 890: "APEX-RBR", 891: "Navis-RBR",
89
+ }
90
+
91
+ # Colors for top profiler type families
92
+ PROFILER_COLORS = {
93
+ "APEX": "#4FC3F7", "Arvor": "#FF7043", "SOLO-SBE": "#26A69A",
94
+ "SOLO2": "#BA68C8", "Deep ARVOR": "#FFB74D", "Provor-SBE": "#00BCD4",
95
+ "S2A": "#F06292", "Navis-A": "#9CCC65", "Provor-MT": "#9575CD",
96
+ "SOLO-FSI": "#FFD54F", "Nova": "#90A4AE", "Ninja": "#EF5350",
97
+ "Arvor-D": "#42A5F5", "Provor-II": "#66BB6A", "Navis-EBR": "#AB47BC",
98
+ "Arvor-CM": "#FFA726", "APEX-Deep-SBE": "#78909C", "APEX-APF11": "#29B6F6",
99
+ "Deep NINJA": "#EC407A", "Deep SOLO-MRV": "#5C6BC0",
100
+ "Other": "#607D8B",
101
+ }
102
+
103
+ # ==================== PAGE CONFIG ====================
104
+ st.set_page_config(
105
+ page_title="Indian ARGO CTD_BGC Dashboard",
106
+ page_icon="🌊",
107
+ layout="wide",
108
+ initial_sidebar_state="expanded",
109
+ menu_items={
110
+ "About": "Indian ARGO CTD/BGC Float Dashboard · INCOIS · Data: IFREMER GDAC"
111
+ },
112
+ )
113
+
114
+
115
+ # ==================== CUSTOM CSS ====================
116
+ st.markdown(
117
+ """
118
+ <style>
119
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap');
120
+
121
+ html, body, [class*="css"] {
122
+ font-family: 'Outfit', 'Inter', sans-serif;
123
+ }
124
+
125
+ /* ── Seamless App Background ── */
126
+ .stApp {
127
+ background: radial-gradient(circle at 10% 20%, rgba(5, 12, 33, 1) 0%, rgba(1, 4, 15, 1) 90%);
128
+ background-attachment: fixed;
129
+ }
130
+
131
+ /* ── Glass Containers ── */
132
+ [data-testid="stMetric"], .kpi-tile, [data-testid="stExpander"], .dac-table, .treemap-info {
133
+ background: rgba(255, 255, 255, 0.04) !important;
134
+ backdrop-filter: blur(12px);
135
+ -webkit-backdrop-filter: blur(12px);
136
+ border: 1px solid rgba(255, 255, 255, 0.08) !important;
137
+ border-radius: 18px !important;
138
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
139
+ padding: 20px;
140
+ }
141
+
142
+ /* ── Sidebar Glass UI ── */
143
+ section[data-testid="stSidebar"] {
144
+ background: rgba(6, 11, 25, 0.82) !important;
145
+ backdrop-filter: blur(15px);
146
+ border-right: 1px solid rgba(0, 188, 212, 0.15);
147
+ }
148
+ section[data-testid="stSidebar"] * { color: #d1d9e6 !important; }
149
+ section[data-testid="stSidebar"] h2 { color: #00BCD4 !important; font-weight: 700; letter-spacing: 0.5px; }
150
+
151
+ /* ── KPI Tiles - Revamped ── */
152
+ .kpi-tile {
153
+ display: flex;
154
+ flex-direction: column;
155
+ align-items: center;
156
+ justify-content: center;
157
+ min-height: 140px;
158
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
159
+ text-align: center;
160
+ border: 1px solid rgba(255, 255, 255, 0.12) !important;
161
+ }
162
+ .kpi-tile:hover {
163
+ transform: translateY(-5px);
164
+ background: rgba(255, 255, 255, 0.07) !important;
165
+ border: 1px solid rgba(0, 188, 212, 0.4) !important;
166
+ box-shadow: 0 12px 40px rgba(0, 188, 212, 0.15);
167
+ }
168
+ .kpi-label {
169
+ font-size: 0.75rem;
170
+ font-weight: 700;
171
+ letter-spacing: 1.2px;
172
+ text-transform: uppercase;
173
+ color: rgba(255, 255, 255, 0.7);
174
+ margin-bottom: 8px;
175
+ }
176
+ .kpi-value {
177
+ font-size: 1.8rem;
178
+ font-weight: 800;
179
+ color: #ffffff;
180
+ white-space: nowrap; /* Prevent wrapping */
181
+ line-height: 1.1;
182
+ }
183
+
184
+ /* ── Header ── */
185
+ .header-bar {
186
+ background: rgba(255, 255, 255, 0.02);
187
+ backdrop-filter: blur(8px);
188
+ border: 1px solid rgba(0, 188, 212, 0.2);
189
+ border-radius: 20px;
190
+ padding: 30px;
191
+ margin-bottom: 25px;
192
+ position: relative;
193
+ }
194
+ .header-bar h1 {
195
+ font-family: 'Outfit', sans-serif;
196
+ font-weight: 800;
197
+ letter-spacing: -0.5px;
198
+ background: linear-gradient(90deg, #ffffff, #00BCD4, #4CAF50);
199
+ -webkit-background-clip: text;
200
+ -webkit-text-fill-color: transparent;
201
+ }
202
+
203
+ /* ── Modern Scrollbar ── */
204
+ ::-webkit-scrollbar { width: 6px; }
205
+ ::-webkit-scrollbar-thumb { background: rgba(0, 188, 212, 0.4); border-radius: 10px; }
206
+
207
+ /* ── Fix Streamlit gaps ── */
208
+ .stPlotlyChart {
209
+ background: rgba(255, 255, 255, 0.02) !important;
210
+ border-radius: 18px;
211
+ padding: 10px;
212
+ border: 1px solid rgba(255, 255, 255, 0.05);
213
+ }
214
+
215
+ /* ── Legend Chips ── */
216
+ .legend-chip {
217
+ display: inline-flex;
218
+ align-items: center;
219
+ gap: 6px;
220
+ padding: 4px 10px;
221
+ margin: 3px 4px;
222
+ background: rgba(255,255,255,0.06);
223
+ border-radius: 12px;
224
+ font-size: 0.75rem;
225
+ color: #c8d6e5;
226
+ border: 1px solid rgba(255,255,255,0.08);
227
+ }
228
+ .legend-dot {
229
+ width: 10px;
230
+ height: 10px;
231
+ border-radius: 50%;
232
+ display: inline-block;
233
+ flex-shrink: 0;
234
+ }
235
+
236
+ /* ── Footer ── */
237
+ .footer-bar {
238
+ text-align: center;
239
+ padding: 20px 30px;
240
+ margin-top: 30px;
241
+ font-size: 0.8rem;
242
+ color: rgba(255,255,255,0.45);
243
+ background: rgba(255,255,255,0.02);
244
+ border-top: 1px solid rgba(0,188,212,0.12);
245
+ border-radius: 16px;
246
+ letter-spacing: 0.3px;
247
+ }
248
+
249
+ /* ── Treemap Summary Stats ── */
250
+ .stat {
251
+ font-size: 1.6rem;
252
+ font-weight: 800;
253
+ color: #ffffff;
254
+ text-align: center;
255
+ }
256
+ .stat-label {
257
+ font-size: 0.7rem;
258
+ text-transform: uppercase;
259
+ letter-spacing: 1px;
260
+ color: rgba(255,255,255,0.5);
261
+ text-align: center;
262
+ margin-top: 2px;
263
+ }
264
+
265
+ /* ── DAC Table ── */
266
+ .dac-table table {
267
+ width: 100%;
268
+ border-collapse: separate;
269
+ border-spacing: 0;
270
+ font-size: 0.85rem;
271
+ color: #c8d6e5;
272
+ }
273
+ .dac-table th {
274
+ padding: 12px 14px;
275
+ text-align: center;
276
+ font-weight: 700;
277
+ color: #00BCD4;
278
+ border-bottom: 2px solid rgba(0,188,212,0.2);
279
+ font-size: 0.8rem;
280
+ letter-spacing: 0.5px;
281
+ text-transform: uppercase;
282
+ }
283
+ .dac-table td {
284
+ padding: 10px 14px;
285
+ text-align: center;
286
+ border-bottom: 1px solid rgba(255,255,255,0.04);
287
+ }
288
+ .dac-table tbody tr:nth-child(odd) {
289
+ background: rgba(255,255,255,0.02);
290
+ }
291
+ .dac-table tbody tr:hover {
292
+ background: rgba(0,188,212,0.06);
293
+ }
294
+
295
+ /* ── Refresh timestamp ── */
296
+ .refresh-ts {
297
+ font-size: 0.75rem;
298
+ color: rgba(255,255,255,0.4);
299
+ margin-top: 6px;
300
+ }
301
+ </style>
302
+ """,
303
+ unsafe_allow_html=True,
304
+ )
305
+
306
+
307
+ # ==================== HELPER: dark plotly layout ====================
308
+ def _dark_layout(**overrides):
309
+ """Return a dark-themed plotly layout dict."""
310
+ base = dict(
311
+ paper_bgcolor="rgba(0,0,0,0)",
312
+ plot_bgcolor="rgba(0,0,0,0)",
313
+ font=dict(family="Inter, sans-serif", color="#c8d6e5", size=12),
314
+ margin=dict(l=40, r=20, t=40, b=40),
315
+ )
316
+ base.update(overrides)
317
+ return base
318
+
319
+
320
+ # ==================== DATA LOADING ====================
321
+ # Global constants for classification
322
+ DEEP_PROFILER_TYPES = {862, 864, 876, 882, 869, 863, 873, 874, 886, 877, 875, 884, 872, 879, 865, 860, 878, 861, 871, 870, 881, 853}
323
+
324
+ @st.cache_data(show_spinner="Loading core-profile index …")
325
+ def load_profile_data():
326
+ """Load ar_index_global_prof.txt with Parquet cache (24-h TTL)."""
327
+ CACHE_DIR.mkdir(exist_ok=True)
328
+ cache_path = CACHE_DIR / "profiles.parquet"
329
+
330
+ if cache_path.exists():
331
+ age_h = (datetime.now().timestamp() - cache_path.stat().st_mtime) / 3600
332
+ if age_h < 24:
333
+ df = pd.read_parquet(cache_path)
334
+ # Ensure is_deep exists (handles stale caches from before this column was added)
335
+ if "is_deep" not in df.columns:
336
+ df["is_deep"] = df["profiler_type"].isin(DEEP_PROFILER_TYPES) if "profiler_type" in df.columns else False
337
+ if "dac" not in df.columns:
338
+ df["dac"] = df["file"].str.extract(r"^([^/]+)/")
339
+ return df
340
+
341
+ df = pd.read_csv(PROF_FILE, comment="#")
342
+ # Strip whitespace from column names (GDAC files sometimes have spaces)
343
+ df.columns = df.columns.str.strip()
344
+
345
+ # --- Land-mask filtering removed: caused discrepancies ---
346
+ df = df.dropna(subset=["latitude", "longitude"])
347
+
348
+ # Ensure coordinates are within valid ranges [-90, 90] and [-180, 180]
349
+ df = df[
350
+ (df["latitude"] >= -90) & (df["latitude"] <= 90) &
351
+ (df["longitude"] >= -180) & (df["longitude"] <= 180)
352
+ ]
353
+
354
+ df["date"] = pd.to_datetime(df["date"], format="%Y%m%d%H%M%S", errors="coerce")
355
+ if "date_update" in df.columns:
356
+ df["date_update"] = pd.to_datetime(
357
+ df["date_update"], format="%Y%m%d%H%M%S", errors="coerce"
358
+ )
359
+ df["wmo_id"] = df["file"].str.extract(r"/(\d+)/")
360
+ df["dac"] = df["file"].str.extract(r"^([^/]+)/")
361
+ df["year"] = df["date"].dt.year
362
+ df["is_deep"] = df["profiler_type"].isin(DEEP_PROFILER_TYPES)
363
+
364
+ df.to_parquet(cache_path, index=False)
365
+ return df
366
+
367
+
368
+ @st.cache_data(show_spinner="Loading BGC-profile index …")
369
+ def load_bio_data():
370
+ """Load argo_bio-profile_index.txt with Parquet cache (24-h TTL)."""
371
+ CACHE_DIR.mkdir(exist_ok=True)
372
+ cache_path = CACHE_DIR / "bgc_profiles.parquet"
373
+
374
+ if cache_path.exists():
375
+ age_h = (datetime.now().timestamp() - cache_path.stat().st_mtime) / 3600
376
+ if age_h < 24:
377
+ return pd.read_parquet(cache_path)
378
+
379
+ df = pd.read_csv(BIO_FILE, comment="#")
380
+ df.columns = df.columns.str.strip()
381
+
382
+ df["date"] = pd.to_datetime(df["date"], format="%Y%m%d%H%M%S", errors="coerce")
383
+ df["wmo_id"] = df["file"].str.extract(r"/(\d+)/")
384
+ df["year"] = df["date"].dt.year
385
+
386
+ params_upper = df["parameters"].fillna("").str.upper()
387
+ df["has_doxy"] = params_upper.str.contains("DOXY")
388
+ df["has_chla"] = params_upper.str.contains("CHLA")
389
+ df["has_nitrate"] = params_upper.str.contains("NITRATE")
390
+ df["has_ph"] = params_upper.str.contains("PH_IN_SITU")
391
+
392
+ df.to_parquet(cache_path, index=False)
393
+ return df
394
+
395
+
396
+ @st.cache_data(show_spinner="Loading float metadata index …")
397
+ def load_meta_data():
398
+ """Load ar_index_global_meta.txt with Parquet cache (24-h TTL).
399
+
400
+ Provides one row per float (WMO) with profiler_type, institution,
401
+ dac, and a human-readable profiler_name from WMO R08.
402
+ """
403
+ CACHE_DIR.mkdir(exist_ok=True)
404
+ cache_path = CACHE_DIR / "meta.parquet"
405
+
406
+ if cache_path.exists():
407
+ age_h = (datetime.now().timestamp() - cache_path.stat().st_mtime) / 3600
408
+ if age_h < 24:
409
+ return pd.read_parquet(cache_path)
410
+
411
+ df = pd.read_csv(META_FILE, comment="#")
412
+ df.columns = df.columns.str.strip()
413
+ df["wmo_id"] = df["file"].str.extract(r"/(\d+)/")
414
+ df["dac"] = df["file"].str.extract(r"^([^/]+)/")
415
+ df["date_update"] = pd.to_datetime(
416
+ df["date_update"], format="%Y%m%d%H%M%S", errors="coerce"
417
+ )
418
+ # Map numeric profiler_type code to human-readable name
419
+ df["profiler_name"] = (
420
+ df["profiler_type"]
421
+ .map(PROFILER_TYPE_NAMES)
422
+ .fillna("Unknown")
423
+ )
424
+ df.to_parquet(cache_path, index=False)
425
+ return df
426
+
427
+
428
+ @st.cache_data
429
+ def _bgc_wmo_set(_df_bio):
430
+ """Set of WMO IDs that have at least one BGC profile."""
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()
437
+ df_bio = load_bio_data()
438
+ df_meta = load_meta_data()
439
+ bgc_wmos = _bgc_wmo_set(df_bio)
440
+
441
+ # Derived column: is this float a BGC float?
442
+ df_prof["is_bgc"] = df_prof["wmo_id"].isin(bgc_wmos)
443
+
444
+ # Enrich profiles with profiler_name from meta (authoritative per-float source)
445
+ _meta_pname = df_meta.set_index("wmo_id")["profiler_name"]
446
+ df_prof["profiler_name"] = df_prof["wmo_id"].map(_meta_pname).fillna("Unknown")
447
+
448
+ @st.dialog("Float Information", width="large")
449
+ def show_float_details(wmo):
450
+ meta_path = BASE_DIR / f"more_components/{wmo}_meta.nc"
451
+ prof_path = BASE_DIR / f"more_components/{wmo}_prof.nc"
452
+
453
+ # Auto-download from IFREMER GDAC if files do not exist
454
+ if not meta_path.exists() or not prof_path.exists():
455
+ import urllib.request
456
+
457
+ dac_row = df_meta[df_meta["wmo_id"] == wmo]
458
+ if len(dac_row) > 0:
459
+ dac = dac_row.iloc[0]["dac"]
460
+ else:
461
+ dac = "incois" # fallback
462
+
463
+ meta_url = f"ftp://ftp.ifremer.fr/ifremer/argo/dac/{dac}/{wmo}/{wmo}_meta.nc"
464
+ prof_url = f"ftp://ftp.ifremer.fr/ifremer/argo/dac/{dac}/{wmo}/{wmo}_prof.nc"
465
+
466
+ target_dir = BASE_DIR / "more_components"
467
+ target_dir.mkdir(exist_ok=True)
468
+
469
+ with st.spinner(f"Downloading GDAC NetCDF files for {wmo} ({dac})..."):
470
+ try:
471
+ if not meta_path.exists():
472
+ urllib.request.urlretrieve(meta_url, meta_path)
473
+ if not prof_path.exists():
474
+ urllib.request.urlretrieve(prof_url, prof_path)
475
+ except Exception as e:
476
+ st.error(f"Failed to download files from {meta_url}. Error: {e}")
477
+ return
478
+
479
+ try:
480
+ ds_meta = xr.open_dataset(meta_path)
481
+ ds_prof = xr.open_dataset(prof_path)
482
+
483
+ def d(val):
484
+ if hasattr(val, "item") and callable(val.item):
485
+ try:
486
+ val = val.item()
487
+ except:
488
+ pass
489
+ if isinstance(val, bytes):
490
+ return val.decode('utf-8', errors='ignore').strip()
491
+ elif isinstance(val, np.ndarray) and val.dtype.kind == 'S':
492
+ return ", ".join([v.decode('utf-8', errors='ignore').strip() for v in val.flat if v.decode('utf-8', errors='ignore').strip()])
493
+ elif isinstance(val, (list, np.ndarray)):
494
+ return ", ".join([d(v) for v in val])
495
+ return str(val).strip()
496
+
497
+ maker = d(ds_meta.PLATFORM_MAKER.values) if 'PLATFORM_MAKER' in ds_meta else 'N/A'
498
+ serial = d(ds_meta.FLOAT_SERIAL_NO.values) if 'FLOAT_SERIAL_NO' in ds_meta else 'N/A'
499
+ ptype = d(ds_meta.PLATFORM_TYPE.values) if 'PLATFORM_TYPE' in ds_meta else 'N/A'
500
+ trans = d(ds_meta.TRANS_SYSTEM.values) if 'TRANS_SYSTEM' in ds_meta else 'N/A'
501
+ owner = d(ds_meta.FLOAT_OWNER.values) if 'FLOAT_OWNER' in ds_meta else 'N/A'
502
+
503
+ dc_map = {
504
+ "AO": "AOML", "BO": "BODC", "CO": "Coriolis", "CS": "CSIRO",
505
+ "IN": "INCOIS", "JA": "JMA", "KM": "KMA", "ME": "MEDS",
506
+ "RU": "RU", "HZ": "CSIO", "NM": "NMDIS"
507
+ }
508
+ if 'DATA_CENTRE' in ds_meta:
509
+ dc_code = d(ds_meta.DATA_CENTRE.values).upper()
510
+ dc = dc_map.get(dc_code, dc_code)
511
+ elif 'OPERATING_INSTITUTION' in ds_meta:
512
+ dc = d(ds_meta.OPERATING_INSTITUTION.values)
513
+ else:
514
+ dc = 'N/A'
515
+ sensors = d(ds_meta.SENSOR.values) if 'SENSOR' in ds_meta else 'N/A'
516
+ ptt = d(ds_meta.PTT.values) if 'PTT' in ds_meta else 'N/A'
517
+
518
+ launch_date = d(ds_meta.LAUNCH_DATE.values) if 'LAUNCH_DATE' in ds_meta else 'N/A'
519
+ if launch_date != 'N/A' and len(launch_date) == 14:
520
+ try:
521
+ dt = datetime.strptime(launch_date, '%Y%m%d%H%M%S')
522
+ launch_date_fmt = dt.strftime('%d/%m/%Y %H:%M:%S')
523
+ age = f"{(datetime.now() - dt).days / 365.25:.2f} years ago"
524
+ except:
525
+ launch_date_fmt = launch_date
526
+ age = "N/A"
527
+ else:
528
+ launch_date_fmt = launch_date
529
+ age = "N/A"
530
+
531
+ launch_lat = float(ds_meta.LAUNCH_LATITUDE.values) if 'LAUNCH_LATITUDE' in ds_meta else 'N/A'
532
+ launch_lon = float(ds_meta.LAUNCH_LONGITUDE.values) if 'LAUNCH_LONGITUDE' in ds_meta else 'N/A'
533
+
534
+ project = d(ds_meta.PROJECT_NAME.values) if 'PROJECT_NAME' in ds_meta else 'N/A'
535
+ pi = d(ds_meta.PI_NAME.values) if 'PI_NAME' in ds_meta else 'N/A'
536
+
537
+ if 'CYCLE_NUMBER' in ds_prof and len(ds_prof.CYCLE_NUMBER) > 0:
538
+ cycle = int(np.nanmax(ds_prof.CYCLE_NUMBER.values))
539
+ juld = ds_prof.JULD.values
540
+ last_date_np = juld[~np.isnat(juld)]
541
+ if len(last_date_np) > 0:
542
+ dt_last = pd.to_datetime(last_date_np[-1])
543
+ last_date = dt_last.strftime('%d/%m/%Y %H:%M:%S')
544
+ if launch_date != 'N/A' and len(launch_date) == 14:
545
+ try:
546
+ dt_launch = datetime.strptime(launch_date, '%Y%m%d%H%M%S')
547
+ cycle_age_years = (dt_last - dt_launch).days / 365.25
548
+ cycle_age = f"{cycle_age_years:.2f} years old"
549
+ except:
550
+ cycle_age = "N/A"
551
+ else:
552
+ cycle_age = "N/A"
553
+ else:
554
+ last_date = "N/A"
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"
579
+ else:
580
+ surf_data = "N/A"
581
+ bott_data = "N/A"
582
+ except:
583
+ surf_data = "N/A"
584
+ bott_data = "N/A"
585
+ else:
586
+ cycle = "N/A"
587
+ last_date = "N/A"
588
+ cycle_age = "N/A"
589
+ surf_data = "N/A"
590
+ bott_data = "N/A"
591
+
592
+ status = "Inactive"
593
+ if last_date != "N/A":
594
+ try:
595
+ dt_last = datetime.strptime(last_date, '%d/%m/%Y %H:%M:%S')
596
+ if (datetime.now() - dt_last).days <= 90:
597
+ status = "Active"
598
+ except:
599
+ pass
600
+
601
+ status_color = "#EF5350" if status == "Inactive" else "#66BB6A"
602
+
603
+ st.markdown("### Main Information")
604
+ st.markdown(f"""
605
+ <div style='display: flex; gap: 20px; flex-wrap: wrap; margin-top: 10px;'>
606
+ <!-- About Float -->
607
+ <div style='flex: 1; min-width: 250px; background: rgba(255,255,255,0.02); padding: 20px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);'>
608
+ <h4 style='color: #4FC3F7; margin-top: 0; font-family: Outfit, sans-serif; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px;'>About Float</h4>
609
+ <table style='width: 100%; border: none; font-size: 0.85em; line-height: 1.5;'>
610
+ <tr>
611
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>WMO<br><span style='color: #4FC3F7; font-size: 1.1em;'>{wmo}</span></td>
612
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Platform maker<br><span style='color: white; font-size: 1.1em;'>{maker}</span></td>
613
+ </tr>
614
+ <tr>
615
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Float serial number<br><span style='color: white; font-size: 1.1em;'>{serial}</span></td>
616
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Platform type<br><span style='color: white; font-size: 1.1em;'>{ptype}</span></td>
617
+ </tr>
618
+ <tr>
619
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Transmission system<br><span style='color: white; font-size: 1.1em;'>{trans}</span></td>
620
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>PTT<br><span style='color: white; font-size: 1.1em;'>{ptt}</span></td>
621
+ </tr>
622
+ <tr>
623
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Owner<br><span style='color: white; font-size: 1.1em;'>{owner}</span></td>
624
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Data Centre<br><span style='color: #4FC3F7; font-size: 1.1em;'>{dc}</span></td>
625
+ </tr>
626
+ <tr>
627
+ <td colspan='2' style='color: rgba(255,255,255,0.5);'>Sensors<br><span style='color: white; font-size: 0.95em;'>{sensors}</span></td>
628
+ </tr>
629
+ </table>
630
+ </div>
631
+ <!-- Deployment -->
632
+ <div style='flex: 1; min-width: 250px; background: rgba(255,255,255,0.02); padding: 20px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);'>
633
+ <h4 style='color: #4FC3F7; margin-top: 0; font-family: Outfit, sans-serif; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px;'>Deployment</h4>
634
+ <table style='width: 100%; border: none; font-size: 0.85em; line-height: 1.5;'>
635
+ <tr>
636
+ <td colspan='2' style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Launched &nbsp; <span style='color: rgba(255,255,255,0.4);'>{age}</span><br><span style='color: white; font-size: 1.1em;'>{launch_date_fmt}</span></td>
637
+ </tr>
638
+ <tr>
639
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Deployment Latitude<br><span style='color: white; font-size: 1.1em;'>{launch_lat}</span></td>
640
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Deployment Longitude<br><span style='color: white; font-size: 1.1em;'>{launch_lon}</span></td>
641
+ </tr>
642
+ <tr>
643
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Ship<br><span style='color: white; font-size: 1.1em;'>frv sagar sampada</span></td>
644
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Cruise<br><span style='color: white; font-size: 1.1em;'></span></td>
645
+ </tr>
646
+ <tr>
647
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Project<br><span style='color: white; font-size: 1.1em;'>{project}</span></td>
648
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Principal Investigator<br><span style='color: white; font-size: 1.1em;'>{pi}</span></td>
649
+ </tr>
650
+ </table>
651
+ </div>
652
+ <!-- Cycle activity -->
653
+ <div style='flex: 1; min-width: 250px; background: rgba(255,255,255,0.02); padding: 20px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);'>
654
+ <h4 style='color: #4FC3F7; margin-top: 0; font-family: Outfit, sans-serif; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px;'>Cycle activity</h4>
655
+ <table style='width: 100%; border: none; font-size: 0.85em; line-height: 1.5;'>
656
+ <tr>
657
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Status<br><span style='color: {status_color}; font-size: 1.1em; font-weight: bold;'>{status}</span></td>
658
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Age<br><span style='color: white; font-size: 1.1em;'>{cycle_age}</span></td>
659
+ </tr>
660
+ <tr>
661
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Last profile date<br><span style='color: white; font-size: 1.1em;'>{last_date}</span></td>
662
+ <td style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Cycle<br><span style='color: white; font-size: 1.1em;'>{cycle}</span></td>
663
+ </tr>
664
+ <tr>
665
+ <td colspan='2' style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Last Surface Data<br><span style='color: white; font-size: 1.05em;'>{surf_data}</span></td>
666
+ </tr>
667
+ <tr>
668
+ <td colspan='2' style='color: rgba(255,255,255,0.5); padding-bottom: 10px;'>Last Bottom Data<br><span style='color: white; font-size: 1.05em;'>{bott_data}</span></td>
669
+ </tr>
670
+ </table>
671
+ </div>
672
+ </div>
673
+ """, unsafe_allow_html=True)
674
+
675
+ st.markdown("---")
676
+ st.markdown("#### Argo parameters section charts and overlaid profiles")
677
+ try:
678
+ import plot_utils
679
+ cycles, dates, pres, temp, psal, rho = plot_utils.get_valid_data(ds_prof)
680
+
681
+ if len(pres) > 0:
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:
711
+ st.error(f"Error rendering technical plots: {e}")
712
+
713
+
714
+ except Exception as e:
715
+ st.error(f"Error loading float details: {e}")
716
+
717
+
718
+ # ==================== HEADER ====================
719
+ _cache_path = CACHE_DIR / "profiles.parquet"
720
+ _last_refresh = (
721
+ datetime.fromtimestamp(_cache_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
722
+ if _cache_path.exists() else "N/A"
723
+ )
724
+ st.markdown(
725
+ f"""
726
+ <div class="header-bar">
727
+ <h1>🌊 Indian ARGO CTD / BGC Dashboard</h1>
728
+ <p>Real-time visibility into the Indian Ocean ARGO float network · Data Source: IFREMER GDAC</p>
729
+ <div class="refresh-ts">Last data refresh: {_last_refresh} UTC</div>
730
+ </div>
731
+ """,
732
+ unsafe_allow_html=True,
733
+ )
734
+
735
+ # ==================== SIDEBAR FILTERS (PRD §6) ====================
736
+ # Read URL query params for shareable filter state
737
+ qp = st.query_params
738
+
739
+ with st.sidebar:
740
+ st.markdown("## 🔍 Filters")
741
+
742
+ # ── Refresh ──
743
+ if st.button("🔄 Refresh Data", use_container_width=True, type="primary"):
744
+ for f in CACHE_DIR.glob("*.parquet"):
745
+ f.unlink()
746
+ st.cache_data.clear()
747
+ st.rerun()
748
+
749
+ st.markdown("---")
750
+
751
+ # ── WMO search ──
752
+ search_wmo = st.text_input(
753
+ "🔎 Search WMO Float ID",
754
+ value=qp.get("wmo", ""),
755
+ placeholder="e.g. 2902115, 2902116",
756
+ help="Comma-separated WMO numbers",
757
+ )
758
+
759
+ # ── QC Mode ──
760
+ _qc_options = ["All", "Delayed", "Real time"]
761
+ _qc_default = _qc_options.index(qp.get("qc", "All")) if qp.get("qc", "All") in _qc_options else 0
762
+ qc_mode = st.selectbox(
763
+ "QC Mode",
764
+ _qc_options,
765
+ index=_qc_default,
766
+ help="All = all data; Delayed = quality-checked; Real time = latest",
767
+ )
768
+
769
+ # ── Community ──
770
+ st.markdown("### Community")
771
+ comm_all = st.checkbox("ALL", value=qp.get("comm_all", "1") == "1", key="comm_all")
772
+ comm_null = st.checkbox("NULL", value=qp.get("comm_null", "0") == "1", key="comm_null")
773
+ comm_argos = st.checkbox("ARGOS", value=qp.get("comm_argos", "0") == "1", key="comm_argos")
774
+ comm_beidou = st.checkbox("BEIDOU", value=qp.get("comm_beidou", "0") == "1", key="comm_beidou")
775
+ comm_iridium = st.checkbox("IRIDIUM", value=qp.get("comm_iridium", "0") == "1", key="comm_iridium")
776
+
777
+ # ── Network ──
778
+ st.markdown("### Network")
779
+ net_all = st.checkbox("All (Inclusive)", value=qp.get("net_all", "1") == "1", key="net_all")
780
+ net_bgc = st.checkbox("BGC (Bio-Argo)", value=qp.get("net_bgc", "0") == "1", key="net_bgc")
781
+ net_ctd = st.checkbox("CTD (Core Argo)", value=qp.get("net_ctd", "0") == "1", key="net_ctd")
782
+ net_dep = st.checkbox("DEP (Deep Argo)", value=qp.get("net_dep", "0") == "1", key="net_dep")
783
+
784
+ # ── Float Model / Profiler Type ──
785
+ st.markdown("### Float Model")
786
+ _available_models = sorted(df_meta["profiler_name"].dropna().unique().tolist())
787
+ selected_profiler_types = st.multiselect(
788
+ "Select Float Model(s)",
789
+ options=_available_models,
790
+ default=[],
791
+ placeholder="All models (no filter)",
792
+ help="Filter by instrument model from metadata registry (WMO R08)",
793
+ )
794
+
795
+ # ── Map Options ──
796
+ st.markdown("### Map Options")
797
+ show_live_only = st.toggle("Live Floats Only (90d)", value=qp.get("live_only", "0") == "1", help="Hide historical dead floats to reduce map clutter")
798
+
799
+ # ── Date range ──
800
+ st.markdown("### Date Range")
801
+ d_col1, d_col2 = st.columns(2)
802
+ with d_col1:
803
+ _min_d = datetime(1960, 1, 1)
804
+ _max_d = datetime.now()
805
+
806
+ # Determine default start date (earliest profile or 1960)
807
+ _default_start = _min_d
808
+ if "df_prof" in locals() and len(df_prof) > 0 and pd.notna(df_prof["date"].min()):
809
+ _default_start = df_prof["date"].min().to_pydatetime()
810
+
811
+ _sd = datetime.strptime(qp.get("sd", ""), "%Y-%m-%d") if "sd" in qp and qp.get("sd", "") else _default_start
812
+ start_date = st.date_input("Start", value=_sd, min_value=_min_d, max_value=_max_d)
813
+ with d_col2:
814
+ _ed = datetime.strptime(qp.get("ed", ""), "%Y-%m-%d") if "ed" in qp and qp.get("ed", "") else _max_d
815
+ end_date = st.date_input("End", value=_ed, min_value=_min_d, max_value=_max_d)
816
+
817
+ # ── Longitude ──
818
+ st.markdown("### Longitude")
819
+ _lon_lo = float(qp.get("lon_lo", "20.0"))
820
+ _lon_hi = float(qp.get("lon_hi", "145.0"))
821
+ lon_range = st.slider(
822
+ "Longitude range",
823
+ min_value=-180.0,
824
+ max_value=180.0,
825
+ value=(_lon_lo, _lon_hi),
826
+ step=0.5,
827
+ label_visibility="collapsed",
828
+ )
829
+
830
+ # ── Latitude ──
831
+ st.markdown("### Latitude")
832
+ _lat_lo = float(qp.get("lat_lo", "-70.1"))
833
+ _lat_hi = float(qp.get("lat_hi", "30.0"))
834
+ lat_range = st.slider(
835
+ "Latitude range",
836
+ min_value=-90.0,
837
+ max_value=90.0,
838
+ value=(_lat_lo, _lat_hi),
839
+ step=0.5,
840
+ label_visibility="collapsed",
841
+ )
842
+
843
+
844
+ # ── Sync current filter state to URL query params ──
845
+ st.query_params.update({
846
+ "wmo": search_wmo,
847
+ "qc": qc_mode,
848
+ "comm_all": "1" if comm_all else "0",
849
+ "comm_null": "1" if comm_null else "0",
850
+ "comm_argos": "1" if comm_argos else "0",
851
+ "comm_beidou": "1" if comm_beidou else "0",
852
+ "comm_iridium": "1" if comm_iridium else "0",
853
+ "net_all": "1" if net_all else "0",
854
+ "net_bgc": "1" if net_bgc else "0",
855
+ "net_ctd": "1" if net_ctd else "0",
856
+ "net_dep": "1" if net_dep else "0",
857
+ "sd": str(start_date),
858
+ "ed": str(end_date),
859
+ "lon_lo": str(lon_range[0]),
860
+ "lon_hi": str(lon_range[1]),
861
+ "lat_lo": str(lat_range[0]),
862
+ "lat_hi": str(lat_range[1]),
863
+ "live_only": "1" if show_live_only else "0",
864
+ })
865
+
866
+ # ==================== FILTER LOGIC (PRD §6.1) ====================
867
+ def apply_filters(df, *, is_bio=False):
868
+ """Apply every sidebar filter to *df* and return the filtered copy."""
869
+ out = df.copy()
870
+
871
+ # Date
872
+ if "date" in out.columns:
873
+ out = out[
874
+ (out["date"] >= pd.Timestamp(start_date))
875
+ & (out["date"] <= pd.Timestamp(end_date))
876
+ ]
877
+
878
+ # Lon / Lat
879
+ if "longitude" in out.columns:
880
+ out = out[
881
+ (out["longitude"] >= lon_range[0]) & (out["longitude"] <= lon_range[1])
882
+ ]
883
+ if "latitude" in out.columns:
884
+ out = out[
885
+ (out["latitude"] >= lat_range[0]) & (out["latitude"] <= lat_range[1])
886
+ ]
887
+
888
+ # Network Logic — only apply to core profiles (bio df lacks is_bgc/is_deep)
889
+ if not net_all and not is_bio and "is_bgc" in out.columns and "is_deep" in out.columns:
890
+ masks = []
891
+ if net_bgc:
892
+ masks.append(out["is_bgc"])
893
+ if net_ctd:
894
+ # Core = NOT BGC and NOT Deep
895
+ masks.append(~out["is_bgc"] & ~out["is_deep"])
896
+ if net_dep:
897
+ masks.append(out["is_deep"])
898
+
899
+ if masks:
900
+ combined_mask = masks[0]
901
+ for m in masks[1:]:
902
+ combined_mask |= m
903
+ out = out[combined_mask]
904
+ elif not (net_bgc or net_ctd or net_dep):
905
+ # If nothing selected and All is off, show nothing
906
+ out = out.iloc[0:0]
907
+
908
+ # WMO search
909
+ if search_wmo.strip():
910
+ wmo_list = [w.strip() for w in search_wmo.split(",") if w.strip()]
911
+ out = out[out["wmo_id"].isin(wmo_list)]
912
+
913
+ # Community Logic
914
+ if not comm_all and "positioning_system" in out.columns:
915
+ masks = []
916
+ if comm_null:
917
+ masks.append(out["positioning_system"].isna() | (out["positioning_system"] == ""))
918
+ if comm_argos:
919
+ masks.append(out["positioning_system"].fillna("").str.upper().str.contains("ARGOS"))
920
+ if comm_beidou:
921
+ masks.append(out["positioning_system"].fillna("").str.upper().str.contains("BEIDOU"))
922
+ if comm_iridium:
923
+ masks.append(out["positioning_system"].fillna("").str.upper().str.contains("IRIDIUM"))
924
+
925
+ if masks:
926
+ combined_mask = masks[0]
927
+ for m in masks[1:]:
928
+ combined_mask |= m
929
+ out = out[combined_mask]
930
+ elif not (comm_null or comm_argos or comm_beidou or comm_iridium):
931
+ out = out.iloc[0:0]
932
+
933
+ # QC mode (bio only)
934
+ if is_bio and "parameter_data_mode" in out.columns:
935
+ if qc_mode == "Delayed":
936
+ out = out[out["parameter_data_mode"].fillna("").str.contains("D")]
937
+ elif qc_mode == "Real time":
938
+ out = out[~out["parameter_data_mode"].fillna("").str.contains("D")]
939
+
940
+ # Profiler Type / Float Model filter (from meta enrichment)
941
+ if selected_profiler_types and "profiler_name" in out.columns:
942
+ out = out[out["profiler_name"].isin(selected_profiler_types)]
943
+
944
+ return out
945
+
946
+
947
+ filt_prof = apply_filters(df_prof)
948
+ filt_bio = apply_filters(df_bio, is_bio=True)
949
+
950
+ # ================================================================
951
+ # FLEET OVERVIEW KPI ROW (from meta registry)
952
+ # ================================================================
953
+ _total_registered = len(df_meta)
954
+ _total_profiled = df_meta["wmo_id"].isin(df_prof["wmo_id"].unique()).sum()
955
+ _never_profiled = _total_registered - _total_profiled
956
+ _unique_models = df_meta["profiler_name"].nunique()
957
+
958
+ st.markdown("### 🛰️ Fleet Overview (from Metadata Registry)")
959
+ fo1, fo2, fo3, fo4 = st.columns(4)
960
+ for col, label, value, color, icon in [
961
+ (fo1, "Registered Floats", _total_registered, "#00BCD4", "📋"),
962
+ (fo2, "Profiled Floats", _total_profiled, "#8BC34A", "✅"),
963
+ (fo3, "Never Profiled", _never_profiled, "#FF5722", "⚠️"),
964
+ (fo4, "Float Models", _unique_models, "#9C27B0", "🔧"),
965
+ ]:
966
+ with col:
967
+ st.markdown(
968
+ f"""
969
+ <div class="kpi-tile" style="border-bottom: 4px solid {color} !important;">
970
+ <div class="kpi-label">{icon} {label}</div>
971
+ <div class="kpi-value">{value:,}</div>
972
+ <div style="font-size: 10px; color: rgba(255,255,255,0.4); margin-top: 4px;">META REGISTRY</div>
973
+ </div>
974
+ """,
975
+ unsafe_allow_html=True,
976
+ )
977
+
978
+ # ================================================================
979
+ # ROW 1 — MAP (left ~55 %) + BAR CHART & KPIs (right ~45 %)
980
+ # ================================================================
981
+ col_left, col_right = st.columns([55, 45], gap="medium")
982
+
983
+ with col_left:
984
+ # ── Component 1: Geospatial Float Position Map (PRD §7.1) ──
985
+ st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
986
+ st.markdown("### 📍 Geographic Float Positions")
987
+
988
+ if len(filt_prof) > 0:
989
+ # --- Check map selection from session state ---
990
+ selected_wmo_from_map = None
991
+ if "main_map" in st.session_state:
992
+ sel = st.session_state.main_map
993
+ if sel and "selection" in sel and "points" in sel["selection"] and len(sel["selection"]["points"]) > 0:
994
+ pt = sel["selection"]["points"][0]
995
+ if "customdata" in pt and len(pt["customdata"]) > 0:
996
+ selected_wmo_from_map = str(pt["customdata"][0])
997
+
998
+ is_sidebar_search = bool(search_wmo.strip())
999
+ is_wmo_searched = is_sidebar_search or bool(selected_wmo_from_map)
1000
+
1001
+ # Apply Live-Only filter if toggled and not searching specific WMOs
1002
+ map_source = filt_prof.copy()
1003
+
1004
+ # If user clicked a float on the map, filter source to just that float
1005
+ if selected_wmo_from_map:
1006
+ map_source = map_source[map_source["wmo_id"] == selected_wmo_from_map]
1007
+
1008
+ if show_live_only and not is_wmo_searched:
1009
+ latest_d = map_source["date"].max()
1010
+ ninety_days_ago = latest_d - timedelta(days=90)
1011
+ # Find WMOs that have a profile in the last 90 days
1012
+ live_wmos = map_source[map_source["date"] >= ninety_days_ago]["wmo_id"].unique()
1013
+ map_source = map_source[map_source["wmo_id"].isin(live_wmos)]
1014
+
1015
+ if is_wmo_searched:
1016
+ # Check if this float is newly selected from map to show dialog
1017
+ if selected_wmo_from_map:
1018
+ if st.session_state.get("last_viewed_wmo") != selected_wmo_from_map:
1019
+ st.session_state["last_viewed_wmo"] = selected_wmo_from_map
1020
+ show_float_details(selected_wmo_from_map)
1021
+
1022
+ # Show full trajectory for specific floats
1023
+ map_df = (
1024
+ map_source.dropna(subset=["latitude", "longitude"])
1025
+ .sort_values(["wmo_id", "date"])
1026
+ .copy()
1027
+ )
1028
+ # Add a profile sequence number for each float
1029
+ map_df["profile_seq"] = map_df.groupby("wmo_id").cumcount() + 1
1030
+
1031
+ fig_map = go.Figure()
1032
+ for wmo, group in map_df.groupby("wmo_id"):
1033
+ inst = group["institution"].iloc[0]
1034
+ color = REGION_COLORS.get(inst, "#ff0000")
1035
+ fig_map.add_trace(go.Scattermapbox(
1036
+ lat=group["latitude"].tolist(),
1037
+ lon=group["longitude"].tolist(),
1038
+ mode="lines+markers+text",
1039
+ text=group["profile_seq"].astype(str).tolist(),
1040
+ customdata=[[wmo]] * len(group),
1041
+ textposition="top right",
1042
+ textfont=dict(size=11, color="white"),
1043
+ marker=dict(size=7, color=color, opacity=0.9),
1044
+ line=dict(width=2, color=color),
1045
+ name=str(wmo),
1046
+ hoverinfo="text",
1047
+ hovertext=group.apply(lambda r: f"WMO: {wmo}<br>Date: {r['date']}<br>Lat: {r['latitude']:.2f}, Lon: {r['longitude']:.2f}<br>Profile: {r['profile_seq']}", axis=1).tolist()
1048
+ ))
1049
+
1050
+ center_lat = float(map_df["latitude"].mean()) if len(map_df) > 0 else 0.0
1051
+ center_lon = float(map_df["longitude"].mean()) if len(map_df) > 0 else 0.0
1052
+
1053
+ fig_map.update_layout(
1054
+ paper_bgcolor="rgba(0,0,0,0)",
1055
+ plot_bgcolor="rgba(0,0,0,0)",
1056
+ margin=dict(l=0, r=0, t=0, b=0),
1057
+ mapbox=dict(
1058
+ style="carto-darkmatter",
1059
+ center=dict(lat=center_lat, lon=center_lon),
1060
+ zoom=4
1061
+ ),
1062
+ legend=dict(
1063
+ title="WMO ID",
1064
+ bgcolor="rgba(10,14,39,0.85)",
1065
+ bordercolor="rgba(0,188,212,0.18)",
1066
+ borderwidth=1,
1067
+ font=dict(size=11, color="#c8d6e5"),
1068
+ yanchor="bottom",
1069
+ y=0.01,
1070
+ xanchor="left",
1071
+ x=0.01,
1072
+ )
1073
+ )
1074
+ else:
1075
+ # Latest position per float (one marker per WMO)
1076
+ map_df = (
1077
+ map_source.dropna(subset=["latitude", "longitude"])
1078
+ .sort_values("date")
1079
+ .groupby("wmo_id")
1080
+ .tail(1)
1081
+ .copy()
1082
+ )
1083
+
1084
+ # Cap at 12 000 for browser performance
1085
+ if len(map_df) > 12_000:
1086
+ map_df = map_df.sample(12_000, random_state=42)
1087
+
1088
+ fig_map = px.scatter_mapbox(
1089
+ map_df,
1090
+ lat="latitude",
1091
+ lon="longitude",
1092
+ color="institution",
1093
+ color_discrete_map=REGION_COLORS,
1094
+ hover_name="wmo_id",
1095
+ custom_data=["wmo_id"],
1096
+ hover_data={
1097
+ "institution": True,
1098
+ "date": True,
1099
+ "latitude": ":.2f",
1100
+ "longitude": ":.2f",
1101
+ },
1102
+ zoom=2,
1103
+ center={"lat": -10, "lon": 80},
1104
+ category_orders={"institution": list(REGION_COLORS.keys())},
1105
+ )
1106
+ fig_map.update_traces(marker=dict(size=8, opacity=0.9))
1107
+ fig_map.update_layout(
1108
+ mapbox_style="carto-darkmatter",
1109
+ paper_bgcolor="rgba(0,0,0,0)",
1110
+ plot_bgcolor="rgba(0,0,0,0)",
1111
+ margin=dict(l=0, r=0, t=0, b=0),
1112
+ legend=dict(
1113
+ title="Region",
1114
+ bgcolor="rgba(10,14,39,0.85)",
1115
+ bordercolor="rgba(0,188,212,0.18)",
1116
+ borderwidth=1,
1117
+ font=dict(size=11, color="#c8d6e5"),
1118
+ yanchor="bottom",
1119
+ y=0.01,
1120
+ xanchor="left",
1121
+ x=0.01,
1122
+ orientation="h",
1123
+ ),
1124
+ )
1125
+
1126
+ fig_map.update_layout(height=620)
1127
+ st.plotly_chart(fig_map, use_container_width=True, key="main_map", on_select="rerun", config={"toImageButtonOptions": {"format": "png", "scale": 2, "filename": "argo_float_map"}})
1128
+
1129
+ if selected_wmo_from_map:
1130
+ if st.button(f"📄 View Info for Float {selected_wmo_from_map}"):
1131
+ show_float_details(selected_wmo_from_map)
1132
+ elif is_sidebar_search and len([w for w in search_wmo.split(",") if w.strip()]) == 1:
1133
+ searched_id = search_wmo.strip()
1134
+ if st.button(f"📄 View Info for Float {searched_id}"):
1135
+ show_float_details(searched_id)
1136
+
1137
+ if is_wmo_searched:
1138
+ st.caption(f"📌 {len(map_df['wmo_id'].unique()):,} floats displayed with full trajectory ({len(map_df):,} total profiles)")
1139
+ else:
1140
+ st.caption(f"📌 {len(map_df):,} unique floats displayed")
1141
+ else:
1142
+ st.info("No float data for current filters.")
1143
+ st.markdown('</div>', unsafe_allow_html=True)
1144
+
1145
+ # ── Component 2 + 3: Bar chart + KPI tiles ──
1146
+ with col_right:
1147
+ st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1148
+ # ── Bar chart (PRD §7.2) ──
1149
+ st.markdown("### 📈 Number of Floats per DAC")
1150
+
1151
+ if len(filt_prof) > 0 and "dac" in filt_prof.columns:
1152
+ # Active floats in the last 90 days of each year
1153
+ latest_ds_date = filt_prof["date"].max()
1154
+ res = []
1155
+ years = sorted(filt_prof["year"].dropna().unique())
1156
+ for y in years:
1157
+ if y == latest_ds_date.year:
1158
+ end_of_year = latest_ds_date
1159
+ else:
1160
+ end_of_year = pd.Timestamp(f"{int(y)}-12-31")
1161
+
1162
+ start_period = end_of_year - pd.Timedelta(days=90)
1163
+ active_df = filt_prof[(filt_prof["date"] >= start_period) & (filt_prof["date"] <= end_of_year)]
1164
+ active_floats = active_df.drop_duplicates(subset=["wmo_id"])
1165
+
1166
+ for dac, count in active_floats["dac"].value_counts().items():
1167
+ res.append({"Year": int(y), "DAC": dac, "Count": count})
1168
+ yearly = pd.DataFrame(res)
1169
+ if len(yearly) > 0:
1170
+ yearly["Year"] = yearly["Year"].astype(int)
1171
+ yearly = yearly.sort_values(["Year", "Count"], ascending=[True, False])
1172
+ totals = yearly.groupby("Year")["Count"].sum().reset_index()
1173
+
1174
+ # Professional DAC Color Mapping
1175
+ DAC_COLORS = {
1176
+ "aoml": "#4FC3F7", "coriolis": "#FF7043", "kiost": "#26A69A",
1177
+ "meds": "#BA68C8", "csiro": "#FFB74D", "jma": "#00BCD4",
1178
+ "incois": "#F06292", "csio": "#9CCC65", "bodc": "#9575CD",
1179
+ "kma": "#FFD54F", "nmdis": "#90A4AE",
1180
+ }
1181
+
1182
+ fig_bar = px.bar(
1183
+ yearly,
1184
+ x="Year",
1185
+ y="Count",
1186
+ color="DAC",
1187
+ color_discrete_map=DAC_COLORS,
1188
+ category_orders={"Year": sorted(yearly["Year"].unique())}
1189
+ )
1190
+
1191
+ fig_bar.update_traces(
1192
+ marker_line_width=0,
1193
+ hovertemplate="<b>%{x}</b><br>DAC: %{fullData.name}<br>Floats: %{y:,}<extra></extra>"
1194
+ )
1195
+
1196
+ fig_bar.add_trace(go.Scatter(
1197
+ x=totals["Year"],
1198
+ y=totals["Count"],
1199
+ mode="text",
1200
+ text=totals["Count"],
1201
+ textposition="top center",
1202
+ textfont=dict(size=10, color="#ffffff", family="Outfit"),
1203
+ showlegend=False,
1204
+ hoverinfo="skip"
1205
+ ))
1206
+
1207
+ fig_bar.update_layout(
1208
+ **_dark_layout(
1209
+ height=420,
1210
+ barmode="stack",
1211
+ xaxis=dict(title="", type="category", tickangle=-45, gridcolor="rgba(255,255,255,0.03)"),
1212
+ yaxis=dict(title="Active Floats", gridcolor="rgba(255,255,255,0.05)", zeroline=False),
1213
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, title=None, font=dict(size=10)),
1214
+ bargap=0.3,
1215
+ margin=dict(l=50, r=20, t=80, b=40),
1216
+ )
1217
+ )
1218
+ st.plotly_chart(fig_bar, use_container_width=True, key="bar_chart", config={"toImageButtonOptions": {"format": "png", "scale": 2, "filename": "argo_annual_floats"}})
1219
+ else:
1220
+ st.info("No active float data for bar chart.")
1221
+ else:
1222
+ st.info("No data for bar chart.")
1223
+
1224
+ # ── KPI tiles (PRD §7.3) ──
1225
+ st.markdown("### 🧪 BGC Profile Counts")
1226
+
1227
+ doxy_n = int(filt_bio["has_doxy"].sum()) if len(filt_bio) > 0 else 0
1228
+ chla_n = int(filt_bio["has_chla"].sum()) if len(filt_bio) > 0 else 0
1229
+ nit_n = int(filt_bio["has_nitrate"].sum()) if len(filt_bio) > 0 else 0
1230
+ ph_n = int(filt_bio["has_ph"].sum()) if len(filt_bio) > 0 else 0
1231
+
1232
+ k1, k2, k3, k4 = st.columns(4)
1233
+ for col, label, value, color in [
1234
+ (k1, "DOXY", doxy_n, KPI_COLORS["DOXY"]),
1235
+ (k2, "Chla", chla_n, KPI_COLORS["Chla"]),
1236
+ (k3, "Nitrate", nit_n, KPI_COLORS["Nitrate"]),
1237
+ (k4, "pH", ph_n, KPI_COLORS["pH"]),
1238
+ ]:
1239
+ with col:
1240
+ st.markdown(
1241
+ f"""
1242
+ <div class="kpi-tile" style="border-bottom: 4px solid {color} !important;" role="status" aria-label="{label}: {value:,} profiles">
1243
+ <div class="kpi-label">{label}</div>
1244
+ <div class="kpi-value">{value:,}</div>
1245
+ <div style="font-size: 10px; color: rgba(255,255,255,0.4); margin-top: 4px;">PROFILES</div>
1246
+ </div>
1247
+ """,
1248
+ unsafe_allow_html=True,
1249
+ )
1250
+ st.markdown('</div>', unsafe_allow_html=True)
1251
+
1252
+ # ================================================================
1253
+ # ROW 2 — TREEMAP (left) + DONUT (right)
1254
+ # ================================================================
1255
+ st.markdown("---")
1256
+ col_tree, col_donut = st.columns(2, gap="medium")
1257
+ # ── Component 4: Active Floats & Profiles last 1 day ──
1258
+ with col_tree:
1259
+ st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1260
+ st.markdown("### 📊 Active Floats & Profiles — Last 1 Day")
1261
+
1262
+ if len(filt_prof) > 0:
1263
+ latest_date = filt_prof["date"].max()
1264
+ one_day_ago = pd.Timestamp(latest_date - timedelta(days=1))
1265
+ last1 = filt_prof[filt_prof["date"] >= one_day_ago].copy()
1266
+
1267
+ if len(last1) > 0:
1268
+ tree_data = (
1269
+ last1.groupby("institution")
1270
+ .agg(floats=("wmo_id", "nunique"), profiles=("file", "count"))
1271
+ .reset_index()
1272
+ )
1273
+
1274
+ total_f1 = int(tree_data["floats"].sum())
1275
+ total_p1 = int(tree_data["profiles"].sum())
1276
+
1277
+ # Summary card
1278
+ st.markdown(
1279
+ f"""
1280
+ <div class="treemap-info">
1281
+ <h3>All Communities</h3>
1282
+ <div style="display:flex;justify-content:space-around;">
1283
+ <div><div class="stat">{total_f1:,}</div>
1284
+ <div class="stat-label">Active Floats</div></div>
1285
+ <div><div class="stat">{total_p1:,}</div>
1286
+ <div class="stat-label">Profiles</div></div>
1287
+ </div>
1288
+ </div>
1289
+ """,
1290
+ unsafe_allow_html=True,
1291
+ )
1292
+
1293
+ # Treemap
1294
+ fig_tree = px.treemap(
1295
+ tree_data,
1296
+ path=["institution"],
1297
+ values="profiles",
1298
+ color="profiles",
1299
+ color_continuous_scale=[
1300
+ [0, "#1a2744"],
1301
+ [0.5, "#1e3a5f"],
1302
+ [1.0, "#2C5F8A"],
1303
+ ],
1304
+ hover_data=["floats", "profiles"],
1305
+ height=340,
1306
+ )
1307
+ fig_tree.update_traces(
1308
+ textinfo="label+value",
1309
+ textfont=dict(size=14, color="white"),
1310
+ marker=dict(line=dict(width=2, color="#0a0e27"), cornerradius=5),
1311
+ hovertemplate=(
1312
+ "<b>%{label}</b><br>"
1313
+ "Profiles: %{value:,}<br>"
1314
+ "Floats: %{customdata[0]:,}<extra></extra>"
1315
+ ),
1316
+ )
1317
+ fig_tree.update_layout(
1318
+ **_dark_layout(margin=dict(l=0, r=0, t=10, b=0)),
1319
+ coloraxis_showscale=False,
1320
+ )
1321
+ st.plotly_chart(fig_tree, use_container_width=True, key="treemap", config={"toImageButtonOptions": {"format": "png", "scale": 2, "filename": "argo_last1day_treemap"}})
1322
+ else:
1323
+ st.info("No active floats in the last 1 day for current filters.")
1324
+ else:
1325
+ st.info("No data available.")
1326
+ st.markdown('</div>', unsafe_allow_html=True)
1327
+
1328
+ # ── Component 5: Float Age Donut (PRD §7.5) ──
1329
+ with col_donut:
1330
+ st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1331
+ st.markdown("### 🕐 Float Age Distribution")
1332
+
1333
+ if len(filt_prof) > 0:
1334
+ # Only calculate age for active floats (reported in the last 90 days)
1335
+ latest_ds_date = filt_prof["date"].max()
1336
+ ninety_days_ago = pd.Timestamp(latest_ds_date - timedelta(days=90))
1337
+
1338
+ float_last = filt_prof.dropna(subset=["date"]).groupby("wmo_id")["date"].max().reset_index()
1339
+ active_wmos = float_last[float_last["date"] >= ninety_days_ago]["wmo_id"]
1340
+
1341
+ active_prof = filt_prof[filt_prof["wmo_id"].isin(active_wmos)]
1342
+
1343
+ # Use earliest profile date per active float as proxy for launch date
1344
+ float_first = (
1345
+ active_prof.dropna(subset=["date"])
1346
+ .groupby("wmo_id")["date"]
1347
+ .min()
1348
+ .reset_index()
1349
+ )
1350
+ float_first["age_years"] = (
1351
+ (pd.Timestamp.now() - float_first["date"]).dt.days / 365.25
1352
+ )
1353
+
1354
+ bins = [0, 3, 6, 9, 12, 999]
1355
+ labels = ["00-02", "03-05", "06-08", "09-11", "12+"]
1356
+ float_first["age_group"] = pd.cut(
1357
+ float_first["age_years"], bins=bins, labels=labels, right=False
1358
+ )
1359
+
1360
+ age_counts = float_first["age_group"].value_counts().reset_index()
1361
+ age_counts.columns = ["Age Group", "Count"]
1362
+ age_counts["Age Group"] = pd.Categorical(
1363
+ age_counts["Age Group"], categories=labels, ordered=True
1364
+ )
1365
+ age_counts = age_counts.sort_values("Age Group")
1366
+ age_counts = age_counts[age_counts["Count"] > 0]
1367
+
1368
+ if len(age_counts) > 0:
1369
+ fig_donut = px.pie(
1370
+ age_counts,
1371
+ values="Count",
1372
+ names="Age Group",
1373
+ hole=0.45,
1374
+ color="Age Group",
1375
+ color_discrete_map=AGE_COLORS,
1376
+ height=420,
1377
+ )
1378
+ fig_donut.update_traces(
1379
+ textinfo="label+percent",
1380
+ textposition="outside",
1381
+ textfont=dict(size=12, color="#c8d6e5"),
1382
+ pull=[0.02] * len(age_counts),
1383
+ hovertemplate=(
1384
+ "<b>%{label}</b><br>"
1385
+ "Count: %{value:,}<br>"
1386
+ "Percent: %{percent}<extra></extra>"
1387
+ ),
1388
+ marker=dict(line=dict(color="#0a0e27", width=2)),
1389
+ )
1390
+ fig_donut.update_layout(
1391
+ **_dark_layout(margin=dict(l=20, r=80, t=10, b=20)),
1392
+ legend=dict(
1393
+ title="Age Group",
1394
+ orientation="v",
1395
+ yanchor="middle",
1396
+ y=0.5,
1397
+ xanchor="left",
1398
+ x=1.05,
1399
+ font=dict(size=12, color="#c8d6e5"),
1400
+ bgcolor="rgba(0,0,0,0)",
1401
+ ),
1402
+ )
1403
+ st.plotly_chart(fig_donut, use_container_width=True, key="donut", config={"toImageButtonOptions": {"format": "png", "scale": 2, "filename": "argo_age_distribution"}})
1404
+ else:
1405
+ st.info("No age data available.")
1406
+ else:
1407
+ st.info("No data available.")
1408
+ st.markdown('</div>', unsafe_allow_html=True)
1409
+
1410
+ # ================================================================
1411
+ # ROW 2.5 — PROFILER TYPE DONUT (left) + FLEET COMPOSITION (right)
1412
+ # Data source: ar_index_global_meta.txt
1413
+ # ================================================================
1414
+ st.markdown("---")
1415
+ col_profiler, col_fleet = st.columns(2, gap="medium")
1416
+
1417
+ # ── Profiler Type / Instrument Breakdown Donut ──
1418
+ with col_profiler:
1419
+ st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1420
+ st.markdown("### 🔧 Float Instrument Types (Meta Registry)")
1421
+
1422
+ if len(df_meta) > 0:
1423
+ ptype_counts = df_meta["profiler_name"].value_counts().reset_index()
1424
+ ptype_counts.columns = ["Model", "Count"]
1425
+
1426
+ # Group small categories into "Other" for readability
1427
+ top_n = 10
1428
+ if len(ptype_counts) > top_n:
1429
+ top = ptype_counts.head(top_n)
1430
+ other_count = ptype_counts.iloc[top_n:]["Count"].sum()
1431
+ other_row = pd.DataFrame([{"Model": "Other", "Count": other_count}])
1432
+ ptype_counts = pd.concat([top, other_row], ignore_index=True)
1433
+
1434
+ fig_ptype = px.pie(
1435
+ ptype_counts,
1436
+ values="Count",
1437
+ names="Model",
1438
+ hole=0.45,
1439
+ color="Model",
1440
+ color_discrete_map=PROFILER_COLORS,
1441
+ height=420,
1442
+ )
1443
+ fig_ptype.update_traces(
1444
+ textinfo="label+percent",
1445
+ textposition="outside",
1446
+ textfont=dict(size=11, color="#c8d6e5"),
1447
+ pull=[0.02] * len(ptype_counts),
1448
+ hovertemplate=(
1449
+ "<b>%{label}</b><br>"
1450
+ "Floats: %{value:,}<br>"
1451
+ "Share: %{percent}<extra></extra>"
1452
+ ),
1453
+ marker=dict(line=dict(color="#0a0e27", width=2)),
1454
+ )
1455
+ fig_ptype.update_layout(
1456
+ **_dark_layout(margin=dict(l=20, r=80, t=10, b=20)),
1457
+ legend=dict(
1458
+ title="Instrument",
1459
+ orientation="v",
1460
+ yanchor="middle",
1461
+ y=0.5,
1462
+ xanchor="left",
1463
+ x=1.05,
1464
+ font=dict(size=11, color="#c8d6e5"),
1465
+ bgcolor="rgba(0,0,0,0)",
1466
+ ),
1467
+ )
1468
+ st.plotly_chart(fig_ptype, use_container_width=True, key="profiler_donut", config={"toImageButtonOptions": {"format": "png", "scale": 2, "filename": "argo_profiler_types"}})
1469
+ st.caption(f"📋 {len(df_meta):,} floats across {df_meta['profiler_name'].nunique()} instrument models (source: ar_index_global_meta.txt)")
1470
+ else:
1471
+ st.info("No metadata available.")
1472
+ st.markdown('</div>', unsafe_allow_html=True)
1473
+
1474
+ # ── Fleet Composition Stacked Area Chart ──
1475
+ with col_fleet:
1476
+ st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1477
+ st.markdown("### 📊 Fleet Composition Over Time")
1478
+
1479
+ if len(filt_prof) > 0 and "profiler_name" in filt_prof.columns:
1480
+ # Get the deployment year per float (earliest profile date)
1481
+ float_deploy = (
1482
+ filt_prof.dropna(subset=["date"])
1483
+ .groupby("wmo_id")
1484
+ .agg(deploy_year=("year", "min"), profiler_name=("profiler_name", "first"))
1485
+ .reset_index()
1486
+ )
1487
+
1488
+ if len(float_deploy) > 0:
1489
+ # Count deployments by year and profiler type
1490
+ comp = float_deploy.groupby(["deploy_year", "profiler_name"]).size().reset_index(name="Count")
1491
+
1492
+ # Keep only top N models, group rest as "Other"
1493
+ top_models = float_deploy["profiler_name"].value_counts().head(8).index.tolist()
1494
+ comp["Model"] = comp["profiler_name"].where(comp["profiler_name"].isin(top_models), "Other")
1495
+ comp = comp.groupby(["deploy_year", "Model"])["Count"].sum().reset_index()
1496
+ comp = comp.sort_values("deploy_year")
1497
+
1498
+ fig_fleet = px.area(
1499
+ comp,
1500
+ x="deploy_year",
1501
+ y="Count",
1502
+ color="Model",
1503
+ color_discrete_map=PROFILER_COLORS,
1504
+ height=420,
1505
+ )
1506
+ fig_fleet.update_traces(
1507
+ line=dict(width=0.5),
1508
+ hovertemplate="<b>%{fullData.name}</b><br>Year: %{x}<br>Floats: %{y:,}<extra></extra>",
1509
+ )
1510
+ fig_fleet.update_layout(
1511
+ **_dark_layout(
1512
+ xaxis=dict(title="Deployment Year", gridcolor="rgba(255,255,255,0.03)"),
1513
+ yaxis=dict(title="Floats Deployed", gridcolor="rgba(255,255,255,0.05)", zeroline=False),
1514
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, title=None, font=dict(size=10)),
1515
+ margin=dict(l=50, r=20, t=60, b=40),
1516
+ ),
1517
+ )
1518
+ st.plotly_chart(fig_fleet, use_container_width=True, key="fleet_composition", config={"toImageButtonOptions": {"format": "png", "scale": 2, "filename": "argo_fleet_composition"}})
1519
+ st.caption("Shows how the fleet instrument mix has evolved per deployment year")
1520
+ else:
1521
+ st.info("No deployment data available.")
1522
+ else:
1523
+ st.info("No data available.")
1524
+ st.markdown('</div>', unsafe_allow_html=True)
1525
+
1526
+ # ================================================================
1527
+ # ROW 3 — DAC / Institution Summary Tables (PRD §7.6)
1528
+ # ================================================================
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
+ )
1538
+ dac_floats = df_meta.groupby("institution").agg(Floats=("wmo_id", "nunique")).reset_index()
1539
+ dac = pd.merge(dac_floats, dac_profs, on="institution", how="left").fillna(0)
1540
+ dac = dac.sort_values("Profiles", ascending=False)
1541
+
1542
+ dacs = dac["institution"].tolist()
1543
+ header = "".join(f"<th>{d}</th>" for d in dacs)
1544
+ floats_cells = "".join(f"<td>{int(r):,}</td>" for r in dac["Floats"])
1545
+ profs_cells = "".join(f"<td>{int(r):,}</td>" for r in dac["Profiles"])
1546
+
1547
+ with col_dac1:
1548
+ st.markdown("### 🏢 DAC / Institution Summary")
1549
+ st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1550
+ st.markdown(
1551
+ f"""
1552
+ <div style="overflow-x:auto;">
1553
+ <table class="dac-table">
1554
+ <thead><tr><th>Metric</th>{header}</tr></thead>
1555
+ <tbody>
1556
+ <tr><td>Floats</td>{floats_cells}</tr>
1557
+ <tr><td>Profiles</td>{profs_cells}</tr>
1558
+ </tbody>
1559
+ </table>
1560
+ </div>
1561
+ """,
1562
+ unsafe_allow_html=True,
1563
+ )
1564
+ st.markdown('</div>', unsafe_allow_html=True)
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(
1574
+ live_floats=("is_live", "sum")
1575
+ ).reset_index()
1576
+
1577
+ status_df = pd.merge(dac_floats.rename(columns={"Floats": "total_count"}), live_df, on="institution", how="left").fillna(0)
1578
+ status_df["dead_floats"] = status_df["total_count"] - status_df["live_floats"]
1579
+ status_df = status_df.set_index("institution").reindex(dacs).reset_index().fillna(0)
1580
+
1581
+ # Dominant instrument per institution from meta registry
1582
+ _inst_top_model = (
1583
+ df_meta.groupby("institution")["profiler_name"]
1584
+ .agg(lambda x: x.value_counts().index[0] if len(x) > 0 else "—")
1585
+ )
1586
+
1587
+ header2 = "".join(f"<th>{d}</th>" for d in status_df["institution"])
1588
+ total_cells = "".join(f"<td>{int(r):,}</td>" for r in status_df["total_count"])
1589
+ live_cells = "".join(f"<td>{int(r):,}</td>" for r in status_df["live_floats"])
1590
+ dead_cells = "".join(f"<td>{int(r):,}</td>" for r in status_df["dead_floats"])
1591
+ model_cells = "".join(
1592
+ f"<td style='font-size:0.75rem;color:#BA68C8;'>{_inst_top_model.get(d, '—')}</td>"
1593
+ for d in status_df["institution"]
1594
+ )
1595
+
1596
+ st.markdown('<div class="stPlotlyChart">', unsafe_allow_html=True)
1597
+ st.markdown(
1598
+ f"""
1599
+ <div style="overflow-x:auto;">
1600
+ <table class="dac-table">
1601
+ <thead><tr><th>Status</th>{header2}</tr></thead>
1602
+ <tbody>
1603
+ <tr><td>Total Count</td>{total_cells}</tr>
1604
+ <tr><td>Live Floats</td>{live_cells}</tr>
1605
+ <tr><td>Dead Floats</td>{dead_cells}</tr>
1606
+ <tr><td>Top Model</td>{model_cells}</tr>
1607
+ </tbody>
1608
+ </table>
1609
+ </div>
1610
+ """,
1611
+ unsafe_allow_html=True,
1612
+ )
1613
+ st.markdown('</div>', unsafe_allow_html=True)
1614
+ 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
+ # ================================================================
1715
+ st.markdown("---")
1716
+ with st.expander("📋 View Raw Data", expanded=False):
1717
+ tab1, tab2, tab3 = st.tabs(["Core Profiles", "BGC Profiles", "Float Metadata"])
1718
+ with tab1:
1719
+ st.dataframe(
1720
+ filt_prof.head(200), use_container_width=True, hide_index=True
1721
+ )
1722
+ st.caption(
1723
+ f"Showing {min(200, len(filt_prof)):,} of {len(filt_prof):,} records"
1724
+ )
1725
+ st.download_button(
1726
+ "⬇️ Download Filtered Core Profiles (CSV)",
1727
+ data=filt_prof.to_csv(index=False),
1728
+ file_name="argo_core_profiles_filtered.csv",
1729
+ mime="text/csv",
1730
+ key="dl_core",
1731
+ )
1732
+ with tab2:
1733
+ st.dataframe(
1734
+ filt_bio.head(200), use_container_width=True, hide_index=True
1735
+ )
1736
+ st.caption(
1737
+ f"Showing {min(200, len(filt_bio)):,} of {len(filt_bio):,} records"
1738
+ )
1739
+ st.download_button(
1740
+ "⬇️ Download Filtered BGC Profiles (CSV)",
1741
+ data=filt_bio.to_csv(index=False),
1742
+ file_name="argo_bgc_profiles_filtered.csv",
1743
+ mime="text/csv",
1744
+ key="dl_bgc",
1745
+ )
1746
+ with tab3:
1747
+ st.dataframe(
1748
+ df_meta.head(500), use_container_width=True, hide_index=True
1749
+ )
1750
+ st.caption(
1751
+ f"Showing {min(500, len(df_meta)):,} of {len(df_meta):,} float metadata records (source: ar_index_global_meta.txt)"
1752
+ )
1753
+ st.download_button(
1754
+ "⬇️ Download Float Metadata (CSV)",
1755
+ data=df_meta.to_csv(index=False),
1756
+ file_name="argo_float_metadata.csv",
1757
+ mime="text/csv",
1758
+ key="dl_meta",
1759
+ )
1760
+
1761
+ # ==================== FOOTER ====================
1762
+ st.markdown(
1763
+ f"""
1764
+ <div class="footer-bar">
1765
+ Indian ARGO CTD/BGC Dashboard · INCOIS · Data: IFREMER GDAC<br>
1766
+ {datetime.now().strftime("%Y-%m-%d %H:%M")} ·
1767
+ {len(df_prof):,} profiles · {df_prof['wmo_id'].nunique():,} floats ·
1768
+ {len(df_bio):,} BGC profiles · {len(df_meta):,} registered floats (meta)
1769
+ </div>
1770
+ """,
1771
+ unsafe_allow_html=True,
1772
+ )
streamlit/dashboard_example.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # argo_dashboard.py
2
+ # Full dashboard (Static map + Bar + Donut + Interactive map + DAC table)
3
+ # - Static & Interactive map use: C:\Users\medagam INDU\Desktop\total dataset.csv
4
+ # - Bar, Donut, Table use: C:\Users\medagam INDU\Desktop\bio_dataset.csv
5
+
6
+ import streamlit as st
7
+ import pandas as pd
8
+ import plotly.express as px
9
+ import matplotlib.pyplot as plt
10
+ import cartopy.crs as ccrs
11
+ import cartopy.feature as cfeature
12
+ from datetime import datetime
13
+
14
+ # ------------------------------
15
+ # UPDATE THESE PATHS if needed
16
+ # ------------------------------
17
+ TOTAL_CSV = r"./total dataset.csv"
18
+ BIO_CSV = r"./bio_dataset.csv"
19
+
20
+ # ------------------------------
21
+ # Page setup
22
+ # ------------------------------
23
+ st.set_page_config(page_title="Argo Float Dashboard", layout="wide")
24
+ # Simple headings only (no extra notes)
25
+ st.title("Argo Float Dashboard")
26
+
27
+ # ------------------------------
28
+ # Helper: detect lat/lon column names
29
+ # ------------------------------
30
+ def detect_latlon(df):
31
+ lat = None
32
+ lon = None
33
+ for c in df.columns:
34
+ cl = c.strip().lower()
35
+ if cl in ("lat", "latitude") or cl.startswith("lat"):
36
+ lat = c
37
+ if cl in ("lon", "long", "longitude") or cl.startswith("lon") or cl.startswith("long"):
38
+ lon = c
39
+ return lat, lon
40
+
41
+ # ------------------------------
42
+ # Load datasets (with caching)
43
+ # ------------------------------
44
+ @st.cache_data
45
+ def load_total():
46
+ df = pd.read_csv(TOTAL_CSV, low_memory=False, encoding='latin1')
47
+ return df
48
+
49
+ @st.cache_data
50
+ def load_bio():
51
+ df = pd.read_csv(BIO_CSV, low_memory=False, encoding='latin1')
52
+ return df
53
+
54
+ total_df = load_total()
55
+ bio_df = load_bio()
56
+
57
+ # ------------------------------
58
+ # Prepare total dataset for static map: try find core/bio type or split
59
+ # ------------------------------
60
+ def prepare_total_map(df, N_split=10):
61
+ d = df.copy()
62
+ # detect lat/lon
63
+ lat_col, lon_col = detect_latlon(d)
64
+ # find a type column if exists
65
+ type_col = None
66
+ for cand in ("Type", "FLOAT_TYPE", "Float_Type", "float_type", "TYPE"):
67
+ if cand in d.columns:
68
+ type_col = cand
69
+ break
70
+ if type_col is not None:
71
+ core = d[d[type_col].astype(str).str.lower().str.contains("core", na=False)].copy()
72
+ bio = d[d[type_col].astype(str).str.lower().str.contains("bio|bgc", na=False)].copy()
73
+ if core.empty and bio.empty:
74
+ core = d.copy()
75
+ bio = d.iloc[0:0].copy()
76
+ else:
77
+ # fallback to Excel-like split (first N columns core, rest bio)
78
+ try:
79
+ core = d.iloc[:, :N_split].copy()
80
+ bio = d.iloc[:, N_split:].copy()
81
+ except Exception:
82
+ core = d.copy()
83
+ bio = d.iloc[0:0].copy()
84
+ return core, bio, lat_col, lon_col
85
+
86
+ core_map_df, bio_map_df, total_lat_col, total_lon_col = prepare_total_map(total_df, N_split=10)
87
+
88
+ # Ensure we have lat/lon for total dataset (fallback common names)
89
+ if total_lat_col is None or total_lon_col is None:
90
+ for a,b in [("LATITUDE","LONGITUDE"), ("LAT","LONG"), ("Latitude","Longitude")]:
91
+ if a in total_df.columns and b in total_df.columns:
92
+ total_lat_col, total_lon_col = a,b
93
+ break
94
+
95
+ # ------------------------------
96
+ # Prepare bio dataset (dates, per-float aggregated)
97
+ # ------------------------------
98
+ def prepare_bio(df):
99
+ b = df.copy()
100
+ # find birth and death columns
101
+ birth_col = None
102
+ death_col = None
103
+ for c in ("DATE_UPDATE","DATE"):
104
+ if c in b.columns:
105
+ birth_col = c
106
+ break
107
+ for c in ("DATE_UPDATE.1","DATE.1","DATE_UPDATE1","DATE_UPDATE_1"):
108
+ if c in b.columns:
109
+ death_col = c
110
+ break
111
+ # fallback search any 'date' columns
112
+ if birth_col is None:
113
+ for c in b.columns:
114
+ if 'date' in c.lower():
115
+ birth_col = c
116
+ break
117
+ # parse dates
118
+ if birth_col is not None:
119
+ b[birth_col] = pd.to_datetime(b[birth_col], errors='coerce', dayfirst=False)
120
+ if death_col is not None:
121
+ b[death_col] = pd.to_datetime(b[death_col], errors='coerce', dayfirst=False)
122
+ else:
123
+ b['DATE_UPDATE.1'] = pd.NaT
124
+ death_col = 'DATE_UPDATE.1'
125
+ # Year for bar chart
126
+ if birth_col is not None:
127
+ b['Year'] = b[birth_col].dt.year
128
+ else:
129
+ b['Year'] = pd.NA
130
+ # id column
131
+ id_col = 'WMOID' if 'WMOID' in b.columns else b.columns[0]
132
+ # per-float aggregation: birth=min(birth_col), death=max(death_col)
133
+ grouped = b.groupby(id_col).agg({
134
+ birth_col: 'min' if birth_col in b.columns else (lambda x: pd.NaT),
135
+ death_col: 'max' if death_col in b.columns else (lambda x: pd.NaT),
136
+ 'DAC': 'first' if 'DAC' in b.columns else (lambda x: None)
137
+ }).reset_index().rename(columns={birth_col: 'birth', death_col: 'death'})
138
+ today = pd.Timestamp.today()
139
+ grouped['end_date'] = grouped['death'].fillna(today)
140
+ grouped['age_days'] = (grouped['end_date'] - grouped['birth']).dt.days
141
+ # classify by 90 days
142
+ grouped['status_90'] = grouped['age_days'].apply(lambda x: 'Live' if pd.notnull(x) and x >= 90 else 'Dead')
143
+ return b, id_col, birth_col, death_col, grouped
144
+
145
+ bio_prepared, bio_id_col, bio_birth_col, bio_death_col, bio_floats_grouped = prepare_bio(bio_df)
146
+
147
+ # ------------------------------
148
+ # Sidebar: filters and search
149
+ # ------------------------------
150
+ st.sidebar.header("Controls")
151
+ # Year filter (bio)
152
+ years_available = sorted([int(y) for y in bio_prepared['Year'].dropna().unique() if pd.notnull(y)])
153
+ selected_years = st.sidebar.multiselect("Year(s) for bar chart", years_available, default=years_available)
154
+ # DAC filter
155
+ dac_options = sorted(bio_prepared['DAC'].dropna().unique().astype(str)) if 'DAC' in bio_prepared.columns else []
156
+ selected_dacs = st.sidebar.multiselect("DAC(s)", dac_options, default=dac_options)
157
+ # Search box
158
+ search_text = st.sidebar.text_input("Search WMOID or DAC")
159
+
160
+ # Apply filters to bio_prepared for bar/donut
161
+ bio_filtered = bio_prepared.copy()
162
+ if selected_years:
163
+ bio_filtered = bio_filtered[bio_filtered['Year'].isin(selected_years)]
164
+ if selected_dacs:
165
+ bio_filtered = bio_filtered[bio_filtered['DAC'].astype(str).isin(selected_dacs)]
166
+ if search_text and search_text.strip():
167
+ bio_filtered = bio_filtered[
168
+ bio_filtered['WMOID'].astype(str).str.contains(search_text, case=False, na=False) |
169
+ bio_filtered['DAC'].astype(str).str.contains(search_text, case=False, na=False)
170
+ ]
171
+
172
+ # Also filter grouped_for_table (per-float) by DAC/search
173
+ grouped_for_table = bio_floats_grouped.copy()
174
+ if selected_dacs:
175
+ grouped_for_table = grouped_for_table[grouped_for_table['DAC'].astype(str).isin(selected_dacs)]
176
+ if search_text and search_text.strip():
177
+ grouped_for_table = grouped_for_table[
178
+ grouped_for_table[bio_id_col].astype(str).str.contains(search_text, case=False, na=False) |
179
+ grouped_for_table['DAC'].astype(str).str.contains(search_text, case=False, na=False)
180
+ ]
181
+
182
+ # ------------------------------
183
+ # Static Map (top) — core pink, bio blue (use total dataset)
184
+ # ------------------------------
185
+ st.subheader("Static Map")
186
+
187
+ plt.figure(figsize=(14,7))
188
+ ax = plt.axes(projection=ccrs.PlateCarree())
189
+ ax.add_feature(cfeature.LAND, facecolor='lightgray')
190
+ ax.add_feature(cfeature.COASTLINE)
191
+ ax.add_feature(cfeature.BORDERS, linestyle=':')
192
+
193
+ # detect lat/lon in core_map_df and bio_map_df returned earlier from total split
194
+ core_lat, core_lon = detect_latlon(core_map_df)
195
+ bio_lat, bio_lon = detect_latlon(bio_map_df)
196
+
197
+ # fallback common names
198
+ if core_lat is None or core_lon is None:
199
+ for a,b in (("LATITUDE","LONGITUDE"), ("LAT","LONG"), ("Latitude","Longitude")):
200
+ if a in core_map_df.columns and b in core_map_df.columns:
201
+ core_lat, core_lon = a,b
202
+ break
203
+ if bio_lat is None or bio_lon is None:
204
+ for a,b in (("LATITUDE","LONGITUDE"), ("LAT","LONG"), ("Latitude","Longitude")):
205
+ if a in bio_map_df.columns and b in bio_map_df.columns:
206
+ bio_lat, bio_lon = a,b
207
+ break
208
+
209
+ # plot if available
210
+ if core_lat and core_lon and (core_lat in core_map_df.columns) and (core_lon in core_map_df.columns):
211
+ ax.scatter(core_map_df[core_lon], core_map_df[core_lat], color='pink', s=15, alpha=0.7, label='Core')
212
+ if bio_lat and bio_lon and (bio_lat in bio_map_df.columns) and (bio_lon in bio_map_df.columns):
213
+ ax.scatter(bio_map_df[bio_lon], bio_map_df[bio_lat], color='blue', s=15, alpha=0.7, label='Bio')
214
+
215
+ plt.title("Core (pink) vs Bio (blue) — Map")
216
+ plt.legend(loc='upper right')
217
+ st.pyplot(plt.gcf())
218
+
219
+ # ------------------------------
220
+ # Middle row: Bar chart (left) and Donut chart (right) — both from bio dataset
221
+ # ------------------------------
222
+ col1, col2 = st.columns(2)
223
+
224
+ # Bar chart: number of unique floats per Year and DAC
225
+ with col1:
226
+ st.subheader("Bar Chart")
227
+ id_col = bio_id_col
228
+ df_grouped = bio_filtered.groupby(['Year','DAC'])[id_col].nunique().reset_index(name='Float_Count')
229
+ totals = df_grouped.groupby('Year')['Float_Count'].sum().reset_index(name='Total_Floats')
230
+
231
+ fig_bar = px.bar(
232
+ df_grouped,
233
+ x='Year',
234
+ y='Float_Count',
235
+ color='DAC',
236
+ text='Float_Count',
237
+ title='Number of Floats per DAC'
238
+ )
239
+ for _, r in totals.iterrows():
240
+ fig_bar.add_annotation(x=r['Year'], y=r['Total_Floats'], text=str(int(r['Total_Floats'])), showarrow=False, yshift=10)
241
+ fig_bar.update_layout(barmode='stack', xaxis=dict(dtick=1), height=520)
242
+ st.plotly_chart(fig_bar, use_container_width=True)
243
+
244
+ # Donut chart: age distribution of alive floats (bio dataset)
245
+ with col2:
246
+ st.subheader("Donut Chart")
247
+ g = bio_floats_grouped.copy()
248
+ # alive = death is NaT
249
+ alive = g[g['death'].isna()].copy()
250
+ if not alive.empty:
251
+ alive['age_years'] = ((pd.Timestamp.today() - alive['birth']).dt.days // 365).astype('Int64')
252
+ alive = alive[alive['age_years'].notna() & (alive['age_years'] >= 0)]
253
+ age_counts = alive['age_years'].value_counts().sort_index().reset_index()
254
+ age_counts.columns = ['Age_Years','Count']
255
+ if not age_counts.empty:
256
+ fig_donut = px.pie(age_counts, names='Age_Years', values='Count', hole=0.55, title='Age (years) distribution of alive floats')
257
+ fig_donut.update_traces(textinfo='percent+label')
258
+ st.plotly_chart(fig_donut, use_container_width=True)
259
+ else:
260
+ st.write("No alive float age groups available for selected filters.")
261
+ else:
262
+ st.write("No alive floats found for selected filters.")
263
+
264
+ # ------------------------------
265
+ # Interactive Plotly Map (uses total dataset)
266
+ # ------------------------------
267
+ # ------------------------------
268
+ # Interactive Plotly Map (ALL DACs always visible)
269
+ # ------------------------------
270
+ st.subheader("Interactive Plotly Map (All DACs)")
271
+
272
+ # copy full total dataset (no DAC filtering)
273
+ map_df = total_df.copy()
274
+
275
+ # detect lat/lon
276
+ lat_t, lon_t = detect_latlon(map_df)
277
+ if lat_t is None and "LATITUDE" in map_df.columns:
278
+ lat_t = "LATITUDE"
279
+ if lon_t is None and "LONGITUDE" in map_df.columns:
280
+ lon_t = "LONGITUDE"
281
+ if lat_t is None and "LAT" in map_df.columns:
282
+ lat_t = "LAT"
283
+ if lon_t is None and "LONG" in map_df.columns:
284
+ lon_t = "LONG"
285
+
286
+ if lat_t is None or lon_t is None:
287
+ st.warning("Latitude/Longitude not found in total dataset for interactive map.")
288
+ else:
289
+ # Convert dates
290
+ if 'DATE_UPDATE' in map_df.columns:
291
+ map_df['DATE_UPDATE'] = pd.to_datetime(map_df['DATE_UPDATE'], errors='coerce')
292
+ if 'DATE_UPDATE.1' in map_df.columns:
293
+ map_df['DATE_UPDATE.1'] = pd.to_datetime(map_df['DATE_UPDATE.1'], errors='coerce')
294
+
295
+ # Compute status (90-day rule)
296
+ if 'DATE_UPDATE' in map_df.columns:
297
+ map_df['end_date'] = map_df['DATE_UPDATE.1'].fillna(pd.Timestamp.today()) \
298
+ if 'DATE_UPDATE.1' in map_df.columns else pd.Timestamp.today()
299
+ map_df['age_days'] = (map_df['end_date'] - map_df['DATE_UPDATE']).dt.days
300
+ map_df['Status'] = map_df['age_days'].apply(
301
+ lambda x: 'Live' if pd.notnull(x) and x >= 90 else 'Dead'
302
+ )
303
+ else:
304
+ map_df['Status'] = 'Unknown'
305
+
306
+ # Hover information
307
+ hover_data = {
308
+ lat_t: True,
309
+ lon_t: True,
310
+ "Status": True
311
+ }
312
+ if "WMOID" in map_df.columns:
313
+ hover_data["WMOID"] = True
314
+ if "DAC" in map_df.columns:
315
+ hover_data["DAC"] = True
316
+ if "DATE_UPDATE" in map_df.columns:
317
+ hover_data["DATE_UPDATE"] = True
318
+ if "DATE_UPDATE.1" in map_df.columns:
319
+ hover_data["DATE_UPDATE.1"] = True
320
+
321
+ # Plot ALL DACs (no filters)
322
+ fig_map = px.scatter_geo(
323
+ map_df,
324
+ lat=lat_t,
325
+ lon=lon_t,
326
+ color="DAC" if "DAC" in map_df.columns else None,
327
+ hover_name="WMOID" if "WMOID" in map_df.columns else None,
328
+ hover_data=hover_data,
329
+ title="Interactive Map — All DACs",
330
+ projection="natural earth"
331
+ )
332
+
333
+ fig_map.update_layout(height=650)
334
+ st.plotly_chart(fig_map, use_container_width=True)
335
+ # ------------------------------
336
+ # DAC summary table (bio dataset) - bottom
337
+ # Live = age_days >= 90, Dead = age_days < 90, Total = unique floats
338
+ # ------------------------------
339
+ st.subheader("DAC Table")
340
+
341
+ g = grouped_for_table.copy()
342
+ # ensure age_days already present (end_date computed earlier)
343
+ g['age_days'] = (g['end_date'] - g['birth']).dt.days
344
+ g['Live90'] = g['age_days'].apply(lambda x: 1 if pd.notnull(x) and x >= 90 else 0)
345
+ g['Dead90'] = g['age_days'].apply(lambda x: 1 if pd.notnull(x) and x < 90 else 0)
346
+
347
+ summary_table = g.groupby('DAC').agg(
348
+ Live=('Live90','sum'),
349
+ Dead=('Dead90','sum'),
350
+ Total=(bio_id_col, 'nunique')
351
+ ).reset_index()
352
+
353
+ # present in requested order: DAC | Live | Dead | Total
354
+ summary_table = summary_table[['DAC','Live','Dead','Total']]
355
+ st.dataframe(summary_table, use_container_width=True)
356
+
357
+ # Compact text lines
358
+ st.markdown("**Compact summary (DAC — Live / Dead / Total)**")
359
+ for _, row in summary_table.iterrows():
360
+ st.write(f"{row['DAC']} — {int(row['Live'])} / {int(row['Dead'])} / {int(row['Total'])}")
streamlit/find_core_logic.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+ target_values = {
5
+ 2014: 973,
6
+ 2016: 1094,
7
+ 2020: 1056,
8
+ }
9
+
10
+ df = pd.read_parquet("cache/profiles.parquet")
11
+ df_io = df[
12
+ (df["longitude"] >= 20.0) & (df["longitude"] <= 145.0) &
13
+ (df["latitude"] >= -70.1) & (df["latitude"] <= 30.0)
14
+ ].copy()
15
+ df_io["date"] = pd.to_datetime(df_io["date"])
16
+ df_io["year"] = df_io["date"].dt.year
17
+
18
+ df_bio = pd.read_parquet("cache/bgc_profiles.parquet")
19
+ bgc_wmos = set(df_bio["wmo_id"].unique())
20
+ DEEP_PROFILER_TYPES = {862, 864, 876, 882, 869, 863, 873, 874, 886, 877, 875, 884, 872, 879, 865, 860, 878, 861, 871, 870, 881, 853}
21
+
22
+ df_io["is_bgc"] = df_io["wmo_id"].isin(bgc_wmos)
23
+ if "profiler_type" in df_io.columns:
24
+ df_io["is_deep"] = df_io["profiler_type"].isin(DEEP_PROFILER_TYPES)
25
+ else:
26
+ df_io["is_deep"] = False
27
+
28
+ print("Total unique active floats in IO:")
29
+ active_yearly = df_io.dropna(subset=["year"]).groupby("year")["wmo_id"].nunique().to_dict()
30
+ for y in target_values: print(f" {y}: Target={target_values[y]}, Calc={active_yearly.get(y, 0)}")
31
+
32
+ print("Core floats only (Not BGC, Not Deep):")
33
+ core_df = df_io[~df_io["is_bgc"] & ~df_io["is_deep"]]
34
+ active_core = core_df.dropna(subset=["year"]).groupby("year")["wmo_id"].nunique().to_dict()
35
+ for y in target_values: print(f" {y}: Target={target_values[y]}, Calc={active_core.get(y, 0)}")
36
+
37
+ print("BGC floats only:")
38
+ bgc_df = df_io[df_io["is_bgc"]]
39
+ active_bgc = bgc_df.dropna(subset=["year"]).groupby("year")["wmo_id"].nunique().to_dict()
40
+ for y in target_values: print(f" {y}: Target={target_values[y]}, Calc={active_bgc.get(y, 0)}")
41
+
42
+ # Let's try matching exactly by seeing if there's a specific month filter or something.
43
+ # Or maybe the data from the image is NOT Indian Ocean, but a specific subset of Global?
streamlit/find_correct_logic.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+ # Target values from the image
5
+ target_values = {
6
+ 2000: 10,
7
+ 2001: 33,
8
+ 2003: 222,
9
+ 2004: 361,
10
+ 2006: 598,
11
+ 2007: 669,
12
+ 2009: 762,
13
+ 2010: 848,
14
+ 2011: 946,
15
+ 2013: 905,
16
+ 2014: 973,
17
+ 2016: 1094,
18
+ 2017: 1029,
19
+ 2018: 1011,
20
+ 2020: 1056,
21
+ 2022: 866,
22
+ 2023: 848,
23
+ 2025: 966
24
+ }
25
+
26
+ print("Loading data...")
27
+ df = pd.read_parquet("cache/profiles.parquet")
28
+ df_io = df[
29
+ (df["longitude"] >= 20.0) & (df["longitude"] <= 145.0) &
30
+ (df["latitude"] >= -70.1) & (df["latitude"] <= 30.0)
31
+ ].copy()
32
+ df_io["date"] = pd.to_datetime(df_io["date"])
33
+ df_io["year"] = df_io["date"].dt.year
34
+
35
+ print("\n--- Testing different metrics to match the target values ---")
36
+
37
+ def compare_to_target(calculated_dict, name):
38
+ print(f"\n{name}")
39
+ diffs = []
40
+ for y, target in target_values.items():
41
+ calc = calculated_dict.get(y, 0)
42
+ diff = calc - target
43
+ diffs.append(abs(diff))
44
+ print(f" {y}: Target={target}, Calc={calc}, Diff={diff}")
45
+ print(f" Avg absolute difference: {np.mean(diffs):.1f}")
46
+
47
+ # 1. Total unique floats per year (active in that year)
48
+ active_yearly = df_io.dropna(subset=["year"]).groupby("year")["wmo_id"].nunique().to_dict()
49
+ compare_to_target(active_yearly, "Metric 1: Active floats per year in Indian Ocean")
50
+
51
+ # 2. Total unique BGC floats per year? No, the title says "No. of Floats", let's try it anyway.
52
+ df_bio = pd.read_parquet("cache/bgc_profiles.parquet")
53
+ # The BGC profiles file doesn't have lat/lon in it in our cache, so we use wmo_id
54
+ bgc_wmos = set(df_bio["wmo_id"].unique())
55
+ active_bgc_yearly = df_io[df_io["wmo_id"].isin(bgc_wmos)].groupby("year")["wmo_id"].nunique().to_dict()
56
+ compare_to_target(active_bgc_yearly, "Metric 2: Active BGC floats per year in Indian Ocean")
57
+
58
+ # 3. Active floats per year globally (no lat/lon filter)
59
+ global_active = df.dropna(subset=["year"]).groupby("year")["wmo_id"].nunique().to_dict()
60
+ compare_to_target(global_active, "Metric 3: Active floats per year GLOBALLY")
61
+
62
+ # 4. Floats that were "Live" at the end of each year?
63
+ # A float is live if it reported a profile in the 90 days before Dec 31 of that year.
64
+ def active_at_end_of_year(df, days=90):
65
+ res = {}
66
+ years = sorted(df["year"].dropna().unique())
67
+ for y in years:
68
+ end_of_year = pd.Timestamp(f"{int(y)}-12-31")
69
+ start_period = end_of_year - pd.Timedelta(days=days)
70
+ # Floats that reported in the last 90 days of the year
71
+ active = df[(df["date"] >= start_period) & (df["date"] <= end_of_year)]["wmo_id"].nunique()
72
+ res[y] = active
73
+ return res
74
+
75
+ compare_to_target(active_at_end_of_year(df_io, 90), "Metric 4: Active in last 90 days of year (IO)")
76
+ compare_to_target(active_at_end_of_year(df_io, 365), "Metric 5: Active in last 365 days of year (IO)")
77
+
78
+ # 6. Core floats only? (Not deep, not BGC)
79
+ core_wmos = set(df_io[~df_io["is_bgc"] & ~df_io["is_deep"]]["wmo_id"])
80
+ active_core = df_io[df_io["wmo_id"].isin(core_wmos)].groupby("year")["wmo_id"].nunique().to_dict()
81
+ compare_to_target(active_core, "Metric 6: Active CORE floats per year (IO)")
82
+
streamlit/hello.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import geopandas as gpd
5
+ from shapely.geometry import Point
6
+
7
+ st.set_page_config(layout="wide")
8
+
9
+ # ---------------- DATA FILES ----------------
10
+ bio_data = "argo_bio-profile_index.txt"
11
+ core_data = "ar_index_global_prof.txt"
12
+
13
+ # ---------------- LOAD DATA ----------------
14
+ @st.cache_data
15
+ def load_data():
16
+ df_bio = pd.read_csv(bio_data, comment="#")
17
+ df_core = pd.read_csv(core_data, comment="#")
18
+ return df_bio, df_core
19
+
20
+ df_bio, df_core = load_data()
21
+
22
+ st.title("Indian ARGO CTD_BGC")
23
+ st.caption("Global profiling float data visualization | Data Source: IFREMER")
24
+
25
+ # ---------------- DATE CONVERSION ----------------
26
+ df_bio['date'] = pd.to_datetime(df_bio['date'], format='%Y%m%d%H%M%S', errors='coerce')
27
+ df_core['date'] = pd.to_datetime(df_core['date'], format='%Y%m%d%H%M%S', errors='coerce')
28
+
29
+ # ---------------- YEAR COLUMN ----------------
30
+ df_bio['year'] = df_bio['date'].dt.year
31
+ df_core['year'] = df_core['date'].dt.year
32
+
33
+ # ---------------- LATEST LOCATION PER FLOAT ----------------
34
+ df_bio_latest = df_bio.sort_values('date').groupby('file').tail(1).copy()
35
+ df_bio_latest['type'] = "BGC"
36
+
37
+ df_core_latest = df_core.sort_values('date').groupby('file').tail(1).copy()
38
+ df_core_latest['type'] = "CTD"
39
+
40
+ # ---------------- COMBINE BOTH ----------------
41
+ df_all = pd.concat([
42
+ df_bio_latest[['latitude','longitude','institution','type','file']],
43
+ df_core_latest[['latitude','longitude','institution','type','file']]
44
+ ], ignore_index=True)
45
+
46
+ # ---------------- CLEAN DATA ----------------
47
+ df_all = df_all.dropna(subset=['latitude','longitude'])
48
+
49
+ # ---------------- GEO FILTER (OCEAN ONLY) ----------------
50
+ @st.cache_data
51
+ def filter_ocean(df):
52
+ geometry = [Point(xy) for xy in zip(df['longitude'], df['latitude'])]
53
+ gdf = gpd.GeoDataFrame(df, geometry=geometry)
54
+
55
+ world = gpd.read_file(
56
+ "https://naturalearth.s3.amazonaws.com/110m_cultural/ne_110m_admin_0_countries.zip"
57
+ )
58
+
59
+ land = world[world['CONTINENT'] != 'Antarctica']
60
+
61
+ gdf_ocean = gdf[~gdf.within(land.geometry.union_all())]
62
+
63
+ gdf_ocean = gdf_ocean[
64
+ (gdf_ocean['latitude'] >= -60) & (gdf_ocean['latitude'] <= 30)
65
+ ]
66
+
67
+ return pd.DataFrame(gdf_ocean.drop(columns='geometry'))
68
+
69
+ df_map_full = filter_ocean(df_all)
70
+
71
+ # ---------------- SIDEBAR FILTER ----------------
72
+ option = st.sidebar.radio(
73
+ "Select Network",
74
+ ["ALL", "BGC", "CTD"]
75
+ )
76
+
77
+ if option == "ALL":
78
+ df_map = df_map_full
79
+ elif option == "BGC":
80
+ df_map = df_map_full[df_map_full['type'] == "BGC"]
81
+ else:
82
+ df_map = df_map_full[df_map_full['type'] == "CTD"]
83
+
84
+ # ---------------- REDUCE DATA SIZE ----------------
85
+ if len(df_map) > 8000:
86
+ df_map = df_map.sample(8000, random_state=42)
87
+
88
+ # ---------------- KPI COUNTS ----------------
89
+ doxy = df_bio[df_bio['parameters'].str.contains("DOXY", na=False)].shape[0]
90
+ chla = df_bio[df_bio['parameters'].str.contains("CHLA", na=False)].shape[0]
91
+ nitrate = df_bio[df_bio['parameters'].str.contains("NITRATE", na=False)].shape[0]
92
+ ph = df_bio[df_bio['parameters'].str.contains("PH", na=False)].shape[0]
93
+
94
+ # ---------------- LAYOUT ----------------
95
+ col1, col2 = st.columns([3, 1])
96
+
97
+ # ---------------- MAP ----------------
98
+ with col1:
99
+ st.subheader("Geographic Distribution (Indian Ocean → Antarctica)")
100
+
101
+ fig_map = px.scatter_mapbox(
102
+ df_map,
103
+ lat="latitude",
104
+ lon="longitude",
105
+ color="type",
106
+ hover_data=["file", "institution", "type"],
107
+ zoom=3,
108
+ height=650
109
+ )
110
+
111
+ fig_map.update_traces(marker=dict(size=5))
112
+
113
+ fig_map.update_layout(
114
+ mapbox_style="open-street-map",
115
+ mapbox=dict(center=dict(lat=-10, lon=80), zoom=3)
116
+ )
117
+
118
+ st.plotly_chart(fig_map, use_container_width=True)
119
+
120
+ # ---------------- KPI ----------------
121
+ with col2:
122
+ st.metric("DOXY Profiles", f"{doxy:,}")
123
+ st.metric("Chla Profiles", f"{chla:,}")
124
+ st.metric("Nitrate Profiles", f"{nitrate:,}")
125
+ st.metric("pH Profiles", f"{ph:,}")
126
+
127
+ # ---------------- BAR CHART ----------------
128
+ st.markdown("---")
129
+
130
+ st.subheader("Number of Floats Deployed Over Years")
131
+
132
+ # combine years
133
+ df_years = pd.concat([
134
+ df_bio[['file','year']],
135
+ df_core[['file','year']]
136
+ ])
137
+
138
+ # remove duplicate floats
139
+ df_years = df_years.drop_duplicates('file')
140
+
141
+ # count per year
142
+ year_counts = df_years.groupby('year').size().reset_index(name='count')
143
+
144
+ # plot
145
+ fig_bar = px.bar(
146
+ year_counts,
147
+ x="year",
148
+ y="count"
149
+ )
150
+
151
+ st.plotly_chart(fig_bar, use_container_width=True)
streamlit/more_components/2900552_meta.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aeaf737d7dec2cc62394c367788f0a986ac6ea37942ecfcea2ad056e263d6775
3
+ size 31332
streamlit/more_components/2900552_prof.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74697fa8421a6f6daefca50680ebf2269c1f916fbb6051077c261b576dce0250
3
+ size 1793024
streamlit/more_components/2902174_meta.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:237a8905395766789208be861c7b115998fd691ded7f00d0c12bda2dd5535934
3
+ size 68748
streamlit/more_components/2902174_prof.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:26ae7ab323dd3cb632986711ae58768f53c29677c4c1c0f7162664dd4c8057f4
3
+ size 18187956
streamlit/more_components/2902771_meta.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2cf8f6dcc5123a617fd1c2f7e171ecb02fdea204cb7f98d28caeca59e7d8b42a
3
+ size 59744
streamlit/more_components/2902771_prof.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:31f4f75b8366dd07dc1a3038c5a0e48879deb97b49e406d31f1b41b494ca6316
3
+ size 3011048
streamlit/more_components/2902821_meta.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:40a6b4c8e0c9f6e6251b662c7e61331b21b84a0f1c5354efb4334c3c1938724d
3
+ size 59744
streamlit/more_components/2902821_prof.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c4910fcaffd8ebaed31f0a75c142df72f99915d130c31e6480952b6630befa2
3
+ size 3364352
streamlit/more_components/2903145_meta.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ad50c0a0b30cb128902418b0da296ac108958b5f958e5ccc28d81b1e0e38155b
3
+ size 52048
streamlit/more_components/2903145_prof.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b2aff0374464a3c07499b18a9004df08c80fce517ca2b0aa314fbfe073f9a7a5
3
+ size 4172636
streamlit/more_components/2903424_meta.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:52126b39e7b1de765ff87bb9923ee16c6e6aae6999a1db6fd512c54d512187c5
3
+ size 38772
streamlit/more_components/2903424_prof.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:738bbb02fff1ea3a4288638a11591d9e1017579c34f98287183b3b2099260af4
3
+ size 13458068
streamlit/more_components/5907180_meta.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:85762274fe81c739d89b03814150253704eb55bbbcd3ce601c23c3c19044969d
3
+ size 31332
streamlit/more_components/5907180_prof.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:051dcf032ff13b60f20aa94359d18067a1f2335be6ee99fbfcf2edde1ac74536
3
+ size 339396
streamlit/more_components/7902408_meta.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1edac7d437d4a6e4478aea5af294b3ff1d5c687f4b7989a8e9aad39544b0ea1d
3
+ size 31332
streamlit/more_components/7902408_prof.nc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c3a4ecf2ebda8497161850733b4ee80222adba9243630312f84b63c1fc7f7547
3
+ size 51016
streamlit/plot_utils.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
24
+ lat_flat = lat_2d.flatten()
25
+
26
+ valid = ~np.isnan(pres) & ~np.isnan(temp) & ~np.isnan(psal) & ~np.isnat(dates)
27
+
28
+ pres = pres[valid]
29
+ temp = temp[valid]
30
+ psal = psal[valid]
31
+ cycles = cycles[valid]
32
+ dates = dates[valid]
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
streamlit/scratch/analyze_meta.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Quick analysis of ar_index_global_meta.txt to understand the data."""
2
+ import pandas as pd
3
+
4
+ META_FILE = r"c:\Users\harsh\incois\dashboard\ar_index_global_meta.txt"
5
+ df = pd.read_csv(META_FILE, comment="#")
6
+ df.columns = df.columns.str.strip()
7
+
8
+ print(f"Total rows: {len(df):,}")
9
+ print(f"Columns: {list(df.columns)}")
10
+ print(f"\nUnique profiler_type codes: {df['profiler_type'].nunique()}")
11
+ print(f"\nProfiler type distribution (top 20):")
12
+ print(df['profiler_type'].value_counts().head(20).to_string())
13
+
14
+ print(f"\n\nUnique institutions: {df['institution'].nunique()}")
15
+ print(f"\nInstitution distribution:")
16
+ print(df['institution'].value_counts().to_string())
17
+
18
+ # Extract DAC and WMO from file path
19
+ df['dac'] = df['file'].str.extract(r'^([^/]+)/')
20
+ df['wmo_id'] = df['file'].str.extract(r'/(\d+)/')
21
+
22
+ print(f"\n\nUnique DACs: {df['dac'].nunique()}")
23
+ print(f"\nDAC distribution:")
24
+ print(df['dac'].value_counts().to_string())
25
+
26
+ print(f"\n\nTotal unique floats (WMOs): {df['wmo_id'].nunique():,}")
27
+
28
+ # Check overlap with prof file
29
+ PROF_FILE = r"c:\Users\harsh\incois\dashboard\ar_index_global_prof.txt"
30
+ df_prof = pd.read_csv(PROF_FILE, comment="#", usecols=['file'])
31
+ df_prof.columns = df_prof.columns.str.strip()
32
+ df_prof['wmo_id'] = df_prof['file'].str.extract(r'/(\d+)/')
33
+ prof_wmos = set(df_prof['wmo_id'].dropna().unique())
34
+ meta_wmos = set(df['wmo_id'].dropna().unique())
35
+
36
+ print(f"\nWMOs in meta but NOT in prof: {len(meta_wmos - prof_wmos):,}")
37
+ print(f"WMOs in prof but NOT in meta: {len(prof_wmos - meta_wmos):,}")
38
+ print(f"WMOs in BOTH: {len(meta_wmos & prof_wmos):,}")
39
+
40
+ # Check which profiler_type codes are NOT in the prof file
41
+ df_prof_full = pd.read_csv(PROF_FILE, comment="#", usecols=['file', 'profiler_type'])
42
+ df_prof_full.columns = df_prof_full.columns.str.strip()
43
+ prof_ptypes = set(df_prof_full['profiler_type'].dropna().unique())
44
+ meta_ptypes = set(df['profiler_type'].dropna().unique())
45
+ print(f"\nProfiler types in meta only: {meta_ptypes - prof_ptypes}")
46
+ print(f"Profiler types in prof only: {prof_ptypes - meta_ptypes}")
streamlit/scratch/check_coordinates.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from pathlib import Path
3
+
4
+ BASE_DIR = Path(r"c:\Users\harsh\incois\dashboard")
5
+ PROF_FILE = BASE_DIR / "ar_index_global_prof.txt"
6
+
7
+ def check_land_points():
8
+ print(f"Reading {PROF_FILE}...")
9
+ df = pd.read_csv(PROF_FILE, comment="#")
10
+ df.columns = df.columns.str.strip()
11
+
12
+ # India land area approx: 8-36N, 68-95E
13
+ india_land = df[
14
+ (df["latitude"] > 8) & (df["latitude"] < 36) &
15
+ (df["longitude"] > 68) & (df["longitude"] < 95)
16
+ ].copy()
17
+
18
+ india_land["wmo_id"] = india_land["file"].str.extract(r"/(\d+)/")
19
+ latest = india_land.sort_values("date").groupby("wmo_id").tail(1)
20
+
21
+ # Specific search for JA floats in this box
22
+ ja_in_india = latest[latest["institution"] == "JA"]
23
+ print(f"JA floats in India box: {len(ja_in_india)}")
24
+ if len(ja_in_india) > 0:
25
+ print(ja_in_india[["wmo_id", "latitude", "longitude", "institution", "date"]].to_string())
26
+
27
+ # All floats in India land box
28
+ print(f"\nAll unique floats in India box: {len(latest)}")
29
+ # Print top 20 suspicious ones (high latitude, inland)
30
+ suspicious = latest[latest["latitude"] > 10]
31
+ print(suspicious[["wmo_id", "latitude", "longitude", "institution", "date"]].head(20))
32
+
33
+ if __name__ == "__main__":
34
+ check_land_points()
streamlit/scratch/investigate_incois_floats.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+
3
+ PROF_FILE = r"c:\Users\harsh\incois\dashboard\ar_index_global_prof.txt"
4
+ META_FILE = r"c:\Users\harsh\incois\dashboard\ar_index_global_meta.txt"
5
+
6
+ df_meta = pd.read_csv(META_FILE, comment="#")
7
+ df_meta.columns = df_meta.columns.str.strip()
8
+ df_meta['wmo_id'] = df_meta['file'].str.extract(r'/(\d+)/')
9
+
10
+ # Filter INCOIS
11
+ meta_in = df_meta[df_meta['institution'] == 'IN']
12
+ meta_wmos = set(meta_in['wmo_id'].unique())
13
+ print(f"Total INCOIS WMOs in meta: {len(meta_wmos)}")
14
+
15
+ # Now check prof file before any filtering
16
+ df_prof_raw = pd.read_csv(PROF_FILE, comment="#", usecols=['file', 'date', 'latitude', 'longitude'])
17
+ df_prof_raw.columns = df_prof_raw.columns.str.strip()
18
+ df_prof_raw['wmo_id'] = df_prof_raw['file'].str.extract(r'/(\d+)/')
19
+
20
+ prof_in_raw = df_prof_raw[df_prof_raw['wmo_id'].isin(meta_wmos)]
21
+ prof_raw_wmos = set(prof_in_raw['wmo_id'].unique())
22
+
23
+ print(f"INCOIS WMOs in raw prof file: {len(prof_raw_wmos)}")
24
+ print(f"Missing from raw prof file: {len(meta_wmos - prof_raw_wmos)}")
25
+ print(meta_wmos - prof_raw_wmos)
26
+
27
+ # Now check after coordinate filtering
28
+ df_prof_coords = prof_in_raw.dropna(subset=['latitude', 'longitude'])
29
+ df_prof_coords = df_prof_coords[
30
+ (df_prof_coords["latitude"] >= -90) & (df_prof_coords["latitude"] <= 90) &
31
+ (df_prof_coords["longitude"] >= -180) & (df_prof_coords["longitude"] <= 180)
32
+ ]
33
+ prof_coord_wmos = set(df_prof_coords['wmo_id'].unique())
34
+ print(f"INCOIS WMOs after coordinate filter: {len(prof_coord_wmos)}")
35
+
36
+ # Now check after date formatting and filtering
37
+ df_prof_coords['date'] = pd.to_datetime(df_prof_coords['date'], format='%Y%m%d%H%M%S', errors='coerce')
38
+ df_prof_dates = df_prof_coords.dropna(subset=['date'])
39
+ prof_date_wmos = set(df_prof_dates['wmo_id'].unique())
40
+ print(f"INCOIS WMOs after valid date filter: {len(prof_date_wmos)}")
streamlit/scratch/test_land_mask.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from global_land_mask import globe
3
+ import warnings
4
+
5
+ warnings.filterwarnings("ignore")
6
+
7
+ PROF_FILE = "ar_index_global_prof.txt"
8
+
9
+ df = pd.read_csv(PROF_FILE, comment="#")
10
+ df.columns = df.columns.str.strip()
11
+ df = df.dropna(subset=["latitude", "longitude"])
12
+ df = df[
13
+ (df["latitude"] >= -90) & (df["latitude"] <= 90) &
14
+ (df["longitude"] >= -180) & (df["longitude"] <= 180)
15
+ ]
16
+
17
+ total_points = len(df)
18
+ is_land = globe.is_land(df["latitude"].values, df["longitude"].values)
19
+ total_land_points = is_land.sum()
20
+
21
+ print(f"Total points: {total_points}")
22
+ print(f"Total points on land globally: {total_land_points}")
23
+
24
+ # India bounding box
25
+ in_india_bbox = (df["latitude"] >= 6.0) & (df["latitude"] <= 36.0) & (df["longitude"] >= 68.0) & (df["longitude"] <= 98.0)
26
+ india_land_points = (in_india_bbox & is_land).sum()
27
+
28
+ print(f"Total points on land in India BBox: {india_land_points}")
streamlit/scratch/test_pivot_dates.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+
3
+ META_FILE = r"c:\Users\harsh\incois\dashboard\ar_index_global_meta.txt"
4
+ PROF_FILE = r"c:\Users\harsh\incois\dashboard\ar_index_global_prof.txt"
5
+
6
+ df_meta = pd.read_csv(META_FILE, comment="#")
7
+ df_meta.columns = df_meta.columns.str.strip()
8
+ df_meta['wmo_id'] = df_meta['file'].str.extract(r'/(\d+)/')
9
+
10
+ df_prof = pd.read_csv(PROF_FILE, comment="#", usecols=['file', 'date', 'institution'])
11
+ df_prof.columns = df_prof.columns.str.strip()
12
+ df_prof['wmo_id'] = df_prof['file'].str.extract(r'/(\d+)/')
13
+ df_prof['date'] = pd.to_datetime(df_prof['date'], format='%Y%m%d%H%M%S', errors='coerce')
14
+
15
+ # Get earliest profile date
16
+ incois_prof = df_prof[df_prof["institution"] == "IN"]
17
+ deployments = incois_prof.groupby("wmo_id")["date"].min().reset_index()
18
+
19
+ # Get all 615 INCOIS meta floats
20
+ meta_in = df_meta[df_meta["institution"] == "IN"].copy()
21
+ print(f"Meta IN WMOs: {len(meta_in)}")
22
+
23
+ # Merge
24
+ merged = pd.merge(meta_in, deployments, on="wmo_id", how="left")
25
+
26
+ # Fill missing dates with date_update
27
+ merged["date_update"] = pd.to_datetime(merged["date_update"], format="%Y%m%d%H%M%S", errors='coerce')
28
+ merged["date_final"] = merged["date"].fillna(merged["date_update"])
29
+
30
+ print(f"Missing dates before fallback: {merged['date'].isna().sum()}")
31
+ print(f"Missing dates after fallback: {merged['date_final'].isna().sum()}")
32
+
33
+ merged["Year"] = merged["date_final"].dt.year
34
+ merged["Month"] = merged["date_final"].dt.month
35
+
36
+ print(f"Missing Year: {merged['Year'].isna().sum()}")
37
+ print(f"Missing Month: {merged['Month'].isna().sum()}")
38
+
39
+ pivot = merged.pivot_table(index="Month", columns="Year", values="wmo_id", aggfunc="count", fill_value=0)
40
+ print(pivot.sum().sum())
streamlit/scratch/verify_filter.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from global_land_mask import globe
3
+ from pathlib import Path
4
+
5
+ PROF_FILE = Path(r"c:\Users\harsh\incois\dashboard\ar_index_global_prof.txt")
6
+
7
+ def verify():
8
+ print("Verifying land mask filtering...")
9
+ df = pd.read_csv(PROF_FILE, comment="#")
10
+ df.columns = df.columns.str.strip()
11
+
12
+ initial_count = len(df)
13
+ print(f"Initial row count: {initial_count:,}")
14
+
15
+ # Filter logic from dashboard.py
16
+ df = df.dropna(subset=["latitude", "longitude"])
17
+
18
+ # Ensure coordinates are within valid ranges for global-land-mask
19
+ df = df[(df["latitude"] >= -90) & (df["latitude"] <= 90) &
20
+ (df["longitude"] >= -180) & (df["longitude"] <= 180)]
21
+
22
+ is_on_land = globe.is_land(df["latitude"].values, df["longitude"].values)
23
+ df_filtered = df[~is_on_land]
24
+
25
+ final_count = len(df_filtered)
26
+ removed_count = initial_count - final_count
27
+ print(f"Final row count: {final_count:,}")
28
+ print(f"Rows removed (on land): {removed_count:,}")
29
+
30
+ # Check if any profiles in the filtered set are still on land
31
+ still_on_land = globe.is_land(df_filtered["latitude"].values, df_filtered["longitude"].values)
32
+ land_count = still_on_land.sum()
33
+ print(f"Profiles remaining in sea-only set that are still on land: {land_count}")
34
+
35
+ if land_count == 0:
36
+ print("\nSUCCESS: No profiles remain on land.")
37
+ else:
38
+ print(f"\nFAILURE: {land_count} profiles are still on land.")
39
+
40
+ if __name__ == "__main__":
41
+ verify()
streamlit/scratch_ftp_test.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import urllib.request
2
+ import os
3
+
4
+ wmo = "2902174"
5
+ dac = "incois"
6
+ meta_url = f"ftp://ftp.ifremer.fr/ifremer/argo/dac/{dac}/{wmo}/{wmo}_meta.nc"
7
+ meta_path = f"{wmo}_meta_test.nc"
8
+
9
+ try:
10
+ print(f"Downloading {meta_url}...")
11
+ urllib.request.urlretrieve(meta_url, meta_path)
12
+ print(f"Success! Size: {os.path.getsize(meta_path)} bytes")
13
+ except Exception as e:
14
+ print("Error:", e)
streamlit/scratch_nc_inspect.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xarray as xr
2
+
3
+ try:
4
+ ds_meta = xr.open_dataset(r"C:\Users\harsh\incois\dashboard\more_components\2902174_meta.nc")
5
+ print("Meta vars:", list(ds_meta.variables))
6
+ for v in ['PLATFORM_NUMBER', 'PLATFORM_MAKER', 'FLOAT_SERIAL_NO', 'PLATFORM_TYPE', 'TRANS_SYSTEM', 'PROJECT_NAME', 'PI_NAME', 'LAUNCH_DATE', 'LAUNCH_LATITUDE', 'LAUNCH_LONGITUDE', 'DATA_CENTRE', 'FIRMWARE_VERSION', 'SENSOR']:
7
+ if v in ds_meta:
8
+ val = ds_meta[v].values
9
+ print(f"{v}: {val}")
10
+ except Exception as e:
11
+ print("Meta error:", e)
12
+
13
+ try:
14
+ ds_prof = xr.open_dataset(r"C:\Users\harsh\incois\dashboard\more_components\2902174_prof.nc")
15
+ print("\nProf vars:", list(ds_prof.variables))
16
+ for v in ['CYCLE_NUMBER', 'JULD', 'PRES', 'TEMP', 'PSAL']:
17
+ if v in ds_prof:
18
+ print(f"{v} shape: {ds_prof[v].shape}")
19
+ except Exception as e:
20
+ print("Prof error:", e)
streamlit/scratch_nc_parse.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xarray as xr
2
+ import numpy as np
3
+ import pandas as pd
4
+ from datetime import datetime
5
+
6
+ ds_meta = xr.open_dataset(r"C:\Users\harsh\incois\dashboard\more_components\2902174_meta.nc")
7
+ ds_prof = xr.open_dataset(r"C:\Users\harsh\incois\dashboard\more_components\2902174_prof.nc")
8
+
9
+ def d(val):
10
+ if hasattr(val, "item") and callable(val.item):
11
+ try:
12
+ val = val.item()
13
+ except:
14
+ pass
15
+ if isinstance(val, bytes):
16
+ return val.decode('utf-8', errors='ignore').strip()
17
+ elif isinstance(val, np.ndarray) and val.dtype.kind == 'S':
18
+ return ", ".join([v.decode('utf-8', errors='ignore').strip() for v in val.flat if v.decode('utf-8', errors='ignore').strip()])
19
+ elif isinstance(val, (list, np.ndarray)):
20
+ return ", ".join([d(v) for v in val])
21
+ return str(val).strip()
22
+
23
+ print("Maker:", d(ds_meta.PLATFORM_MAKER.values))
24
+ print("Serial:", d(ds_meta.FLOAT_SERIAL_NO.values))
25
+ print("Type:", d(ds_meta.PLATFORM_TYPE.values))
26
+ print("Trans:", d(ds_meta.TRANS_SYSTEM.values))
27
+ print("DC:", d(ds_meta.DATA_CENTRE.values))
28
+ print("Sensors:", d(ds_meta.SENSOR.values))
29
+ print("PTT:", d(ds_meta.PTT.values) if 'PTT' in ds_meta else "N/A")
30
+ print("Launch date:", d(ds_meta.LAUNCH_DATE.values))
31
+ print("Project:", d(ds_meta.PROJECT_NAME.values))
32
+ print("PI:", d(ds_meta.PI_NAME.values))
33
+ print("Lat:", float(ds_meta.LAUNCH_LATITUDE.values))
34
+
35
+ cycle = int(np.nanmax(ds_prof.CYCLE_NUMBER.values))
36
+ juld = ds_prof.JULD.values
37
+ last_date_np = juld[~np.isnat(juld)]
38
+ last_date = pd.to_datetime(last_date_np[-1]).strftime('%d/%m/%Y %H:%M:%S')
39
+
40
+ last_pres = ds_prof.PRES.values[-1]
41
+ last_temp = ds_prof.TEMP.values[-1]
42
+ last_psal = ds_prof.PSAL.values[-1]
43
+
44
+ valid_idx = ~np.isnan(last_pres)
45
+ pres_v = last_pres[valid_idx]
46
+ temp_v = last_temp[valid_idx]
47
+ psal_v = last_psal[valid_idx]
48
+
49
+ surface_idx = np.argmin(pres_v)
50
+ bottom_idx = np.argmax(pres_v)
51
+ print(f"Cycle: {cycle}, Last Date: {last_date}")
52
+ print(f"Surface: {pres_v[surface_idx]} dbar {temp_v[surface_idx]} C {psal_v[surface_idx]} PSU")
53
+ print(f"Bottom: {pres_v[bottom_idx]} dbar {temp_v[bottom_idx]} C {psal_v[bottom_idx]} PSU")
streamlit/scratch_nc_vars.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xarray as xr
2
+ ds = xr.open_dataset(r"C:\Users\harsh\incois\dashboard\more_components\2902174_meta.nc")
3
+ for v in ds.variables:
4
+ if ds[v].dtype.kind in ['S', 'U', 'O']:
5
+ try:
6
+ val = ds[v].values
7
+ if hasattr(val, "item") and callable(val.item) and val.ndim == 0:
8
+ val = val.item()
9
+ if isinstance(val, bytes):
10
+ val = val.decode('utf-8', errors='ignore').strip()
11
+ elif isinstance(val, np.ndarray) and val.dtype.kind == 'S':
12
+ val = ", ".join([x.decode('utf-8', errors='ignore').strip() for x in val.flat if x.decode('utf-8', errors='ignore').strip()])
13
+ else:
14
+ val = str(val).strip()
15
+ print(f"{v}: {val}")
16
+ except Exception as e:
17
+ pass
streamlit/scratch_nc_vars_2903424.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xarray as xr
2
+ import urllib.request
3
+ from pathlib import Path
4
+ import numpy as np
5
+
6
+ wmo = "2903424"
7
+ dac = "aoml"
8
+ meta_url = f"ftp://ftp.ifremer.fr/ifremer/argo/dac/{dac}/{wmo}/{wmo}_meta.nc"
9
+ meta_path = f"more_components/{wmo}_meta.nc"
10
+
11
+ if not Path(meta_path).exists():
12
+ urllib.request.urlretrieve(meta_url, meta_path)
13
+
14
+ ds_meta = xr.open_dataset(meta_path)
15
+ for v in ds_meta.variables:
16
+ if ds_meta[v].dtype.kind in ['S', 'U', 'O']:
17
+ try:
18
+ val = ds_meta[v].values
19
+ if hasattr(val, "item") and callable(val.item) and val.ndim == 0:
20
+ val = val.item()
21
+ if isinstance(val, bytes):
22
+ val = val.decode('utf-8', errors='ignore').strip()
23
+ elif isinstance(val, np.ndarray) and val.dtype.kind == 'S':
24
+ val = ", ".join([x.decode('utf-8', errors='ignore').strip() for x in val.flat if x.decode('utf-8', errors='ignore').strip()])
25
+ else:
26
+ val = str(val).strip()
27
+ print(f"{v}: {val}")
28
+ except Exception as e:
29
+ pass
streamlit/scratch_nc_vars_2903424_specific.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xarray as xr
2
+ import numpy as np
3
+
4
+ wmo = "2903424"
5
+ meta_path = f"more_components/{wmo}_meta.nc"
6
+
7
+ ds_meta = xr.open_dataset(meta_path)
8
+ for v in ds_meta.variables:
9
+ if any(kw in v for kw in ["OWNER", "CENTRE", "INST", "DAC", "PI", "PROJECT"]):
10
+ try:
11
+ val = ds_meta[v].values
12
+ if hasattr(val, "item") and callable(val.item) and val.ndim == 0:
13
+ val = val.item()
14
+ if isinstance(val, bytes):
15
+ val = val.decode('utf-8', errors='ignore').strip()
16
+ elif isinstance(val, np.ndarray) and val.dtype.kind == 'S':
17
+ val = ", ".join([x.decode('utf-8', errors='ignore').strip() for x in val.flat if x.decode('utf-8', errors='ignore').strip()])
18
+ else:
19
+ val = str(val).strip()
20
+ print(f"{v}: {val}")
21
+ except Exception as e:
22
+ pass
streamlit/scratch_plot_test.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import xarray as xr
2
+ import plot_utils
3
+
4
+ try:
5
+ ds_prof = xr.open_dataset(r"C:\Users\harsh\incois\dashboard\more_components\2902821_prof.nc")
6
+ cycles, dates, pres, temp, psal, rho = plot_utils.get_valid_data(ds_prof)
7
+ print("Success. Extracted valid data shapes:")
8
+ print("PRES:", pres.shape)
9
+ except Exception as e:
10
+ print("Error:", e)
streamlit/scratch_prof_test.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xarray as xr
2
+ import numpy as np
3
+
4
+ ds_prof = xr.open_dataset(r"C:\Users\harsh\incois\dashboard\more_components\2902174_prof.nc")
5
+
6
+ print("Checking last few cycles for valid PRES data...")
7
+ pres_data = ds_prof.PRES.values
8
+ valid_cycles = np.where(~np.isnan(pres_data).all(axis=1))[0]
9
+
10
+ print(f"Total cycles: {pres_data.shape[0]}")
11
+ print(f"Total valid cycles: {len(valid_cycles)}")
12
+
13
+ if len(valid_cycles) > 0:
14
+ last_valid_idx = valid_cycles[-1]
15
+ print(f"Last valid cycle index: {last_valid_idx}, Cycle number: {ds_prof.CYCLE_NUMBER.values[last_valid_idx]}")
16
+
17
+ last_pres = pres_data[last_valid_idx]
18
+ last_temp = ds_prof.TEMP.values[last_valid_idx] if 'TEMP' in ds_prof else np.full_like(last_pres, np.nan)
19
+ last_psal = ds_prof.PSAL.values[last_valid_idx] if 'PSAL' in ds_prof else np.full_like(last_pres, np.nan)
20
+
21
+ valid_idx = ~np.isnan(last_pres)
22
+ pres_v = last_pres[valid_idx]
23
+ temp_v = last_temp[valid_idx]
24
+ psal_v = last_psal[valid_idx]
25
+
26
+ print(f"Found {len(pres_v)} valid pressure points in this cycle.")
27
+
28
+ if len(pres_v) > 0:
29
+ surface_idx = np.argmin(pres_v)
30
+ bottom_idx = np.argmax(pres_v)
31
+ print(f"Surface: {pres_v[surface_idx]:.2f} dbar {temp_v[surface_idx]:.3f}°C {psal_v[surface_idx]:.3f} PSU")
32
+ print(f"Bottom: {pres_v[bottom_idx]:.2f} dbar {temp_v[bottom_idx]:.3f}°C {psal_v[bottom_idx]:.3f} PSU")
33
+
34
+ ds_meta = xr.open_dataset(r"C:\Users\harsh\incois\dashboard\more_components\2902174_meta.nc")
35
+ def d(val):
36
+ if hasattr(val, "item") and callable(val.item) and val.ndim == 0:
37
+ val = val.item()
38
+ if isinstance(val, bytes):
39
+ return val.decode('utf-8', errors='ignore').strip()
40
+ elif isinstance(val, np.ndarray) and val.dtype.kind == 'S':
41
+ return ", ".join([v.decode('utf-8', errors='ignore').strip() for v in val.flat if v.decode('utf-8', errors='ignore').strip()])
42
+ return str(val).strip()
43
+
44
+ print("\nData Centre fields:")
45
+ print(f"DATA_CENTRE: {d(ds_meta.DATA_CENTRE.values) if 'DATA_CENTRE' in ds_meta else 'N/A'}")
46
+ print(f"OPERATING_INSTITUTION: {d(ds_meta.OPERATING_INSTITUTION.values) if 'OPERATING_INSTITUTION' in ds_meta else 'N/A'}")
47
+
streamlit/test_counts.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+
3
+ # Load data
4
+ print("Loading data...")
5
+ df = pd.read_parquet("cache/profiles.parquet")
6
+
7
+ # Filter to Indian Ocean (default dashboard filter)
8
+ df_io = df[
9
+ (df["longitude"] >= 20.0) & (df["longitude"] <= 145.0) &
10
+ (df["latitude"] >= -70.1) & (df["latitude"] <= 30.0)
11
+ ]
12
+
13
+ print("\n--- Metric 1: New Floats Deployed per Year (Current Dashboard Logic) ---")
14
+ float_years = df_io.dropna(subset=["year"]).groupby("wmo_id")["year"].min().reset_index()
15
+ yearly_new = float_years.groupby("year")["wmo_id"].nunique().reset_index()
16
+ print(yearly_new[yearly_new["year"].isin([1999, 2001, 2003, 2011, 2014, 2016, 2020, 2025, 2026])].to_string(index=False))
17
+
18
+ print("\n--- Metric 2: Active Floats per Year (Unique WMOs reporting in that year) ---")
19
+ yearly_active = df_io.dropna(subset=["year"]).groupby("year")["wmo_id"].nunique().reset_index()
20
+ print(yearly_active[yearly_active["year"].isin([1999, 2000, 2001, 2003, 2004, 2008, 2016, 2020, 2026])].to_string(index=False))
streamlit/test_last7.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from datetime import timedelta
3
+
4
+ print('Loading data...')
5
+ df = pd.read_parquet('cache/profiles.parquet')
6
+
7
+ print('\n--- Indian Ocean last 7 days ---')
8
+ df_io = df[
9
+ (df['longitude'] >= 20.0) & (df['longitude'] <= 145.0) &
10
+ (df['latitude'] >= -70.1) & (df['latitude'] <= 30.0)
11
+ ]
12
+ latest_io = df_io['date'].max()
13
+ last7_io = df_io[df_io['date'] >= latest_io - timedelta(days=7)]
14
+ print(f"Floats: {last7_io['wmo_id'].nunique()}")
15
+ print(f"Profiles: {len(last7_io)}")
16
+
17
+ print('\n--- Indian Ocean last 7 days by institution ---')
18
+ tree_data = last7_io.groupby('institution').agg(floats=('wmo_id', 'nunique'), profiles=('file', 'count')).reset_index().sort_values('floats', ascending=False)
19
+ print(tree_data.to_string(index=False))
20
+
21
+ print('\n--- Let us also try with ONLY BGC floats? No, the image says All Communities ---')