Spaces:
Sleeping
Sleeping
harshini9942 commited on
Commit ·
0ebe8a8
1
Parent(s): db19753
Streamlit Dashboard Pushed 20-05-2026
Browse files- streamlit/.gitignore +1 -0
- streamlit/.streamlit/config.toml +11 -0
- streamlit/2902174_meta_test.nc +3 -0
- streamlit/ARGO_Dashboard_PRD.docx +0 -0
- streamlit/__pycache__/plot_utils.cpython-314.pyc +0 -0
- streamlit/analyze_data.py +85 -0
- streamlit/cache/bgc_profiles.parquet +3 -0
- streamlit/cache/meta.parquet +3 -0
- streamlit/cache/profiles.parquet +3 -0
- streamlit/dashboard.py +1772 -0
- streamlit/dashboard_example.py +360 -0
- streamlit/find_core_logic.py +43 -0
- streamlit/find_correct_logic.py +82 -0
- streamlit/hello.py +151 -0
- streamlit/more_components/2900552_meta.nc +3 -0
- streamlit/more_components/2900552_prof.nc +3 -0
- streamlit/more_components/2902174_meta.nc +3 -0
- streamlit/more_components/2902174_prof.nc +3 -0
- streamlit/more_components/2902771_meta.nc +3 -0
- streamlit/more_components/2902771_prof.nc +3 -0
- streamlit/more_components/2902821_meta.nc +3 -0
- streamlit/more_components/2902821_prof.nc +3 -0
- streamlit/more_components/2903145_meta.nc +3 -0
- streamlit/more_components/2903145_prof.nc +3 -0
- streamlit/more_components/2903424_meta.nc +3 -0
- streamlit/more_components/2903424_prof.nc +3 -0
- streamlit/more_components/5907180_meta.nc +3 -0
- streamlit/more_components/5907180_prof.nc +3 -0
- streamlit/more_components/7902408_meta.nc +3 -0
- streamlit/more_components/7902408_prof.nc +3 -0
- streamlit/plot_utils.py +86 -0
- streamlit/scratch/analyze_meta.py +46 -0
- streamlit/scratch/check_coordinates.py +34 -0
- streamlit/scratch/investigate_incois_floats.py +40 -0
- streamlit/scratch/test_land_mask.py +28 -0
- streamlit/scratch/test_pivot_dates.py +40 -0
- streamlit/scratch/verify_filter.py +41 -0
- streamlit/scratch_ftp_test.py +14 -0
- streamlit/scratch_nc_inspect.py +20 -0
- streamlit/scratch_nc_parse.py +53 -0
- streamlit/scratch_nc_vars.py +17 -0
- streamlit/scratch_nc_vars_2903424.py +29 -0
- streamlit/scratch_nc_vars_2903424_specific.py +22 -0
- streamlit/scratch_plot_test.py +10 -0
- streamlit/scratch_prof_test.py +47 -0
- streamlit/test_counts.py +20 -0
- 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 <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 ---')
|