Spaces:
Sleeping
Sleeping
rickyt
commited on
Commit
Β·
7464be9
1
Parent(s):
ce62384
dark mode
Browse files
app.py
CHANGED
|
@@ -21,7 +21,7 @@ def _wx_emoji(txt):
|
|
| 21 |
"""Safely format weather text with emoji icons"""
|
| 22 |
if txt is None or (isinstance(txt, float) and np.isnan(txt)):
|
| 23 |
return "β"
|
| 24 |
-
txt = str(txt) # ensure
|
| 25 |
t = txt.lower()
|
| 26 |
if "thunder" in t: return "βοΈ " + txt
|
| 27 |
if "shower" in t or "rain" in t or "drizzle" in t: return "π§οΈ " + txt
|
|
@@ -30,7 +30,6 @@ def _wx_emoji(txt):
|
|
| 30 |
if "clear" in t or "sun" in t: return "βοΈ " + txt
|
| 31 |
return txt
|
| 32 |
|
| 33 |
-
|
| 34 |
def _fmt(x, nd=1):
|
| 35 |
if x is None or (isinstance(x, float) and np.isnan(x)): return "β"
|
| 36 |
if isinstance(x, (int, np.integer)): return f"{x:d}"
|
|
@@ -38,15 +37,18 @@ def _fmt(x, nd=1):
|
|
| 38 |
return escape(str(x))
|
| 39 |
|
| 40 |
def _heat(value, vmax, hue="blue"):
|
|
|
|
| 41 |
try:
|
| 42 |
v = float(value or 0.0)
|
| 43 |
except:
|
| 44 |
v = 0.0
|
| 45 |
pct = max(0.0, min(1.0, v / max(vmax, 1e-9)))
|
| 46 |
if hue == "blue":
|
| 47 |
-
|
|
|
|
| 48 |
if hue == "green":
|
| 49 |
-
|
|
|
|
| 50 |
return ""
|
| 51 |
|
| 52 |
def build_pretty_table(df: pd.DataFrame) -> tuple[str, dict]:
|
|
@@ -54,18 +56,18 @@ def build_pretty_table(df: pd.DataFrame) -> tuple[str, dict]:
|
|
| 54 |
return "<p>No data.</p>", {}
|
| 55 |
|
| 56 |
df = df.copy()
|
| 57 |
-
df["XW_mm"]
|
| 58 |
df["TMRW_mm"] = pd.to_numeric(df.get("TMRW_PrecipRate_mmph"), errors="coerce")
|
| 59 |
-
df["XW_PoP"]
|
| 60 |
-
df["TMRW_PoP"]
|
| 61 |
|
| 62 |
-
# Rain alert
|
| 63 |
rain_flag = (
|
| 64 |
((df["XW_mm"] >= 1.0) | (df["TMRW_mm"] >= 1.0)) |
|
| 65 |
((df["XW_PoP"] >= 70) | (df["TMRW_PoP"] >= 70))
|
| 66 |
)
|
| 67 |
|
| 68 |
-
# Next rain
|
| 69 |
def _first_rain_row():
|
| 70 |
for _, row in df.iterrows():
|
| 71 |
if (row.get("XW_mm", 0) >= 1) or (row.get("TMRW_mm", 0) >= 1) or \
|
|
@@ -74,14 +76,14 @@ def build_pretty_table(df: pd.DataFrame) -> tuple[str, dict]:
|
|
| 74 |
return None
|
| 75 |
next_rain = _first_rain_row()
|
| 76 |
|
|
|
|
| 77 |
xw_vals = df["XW_mm"].fillna(0).to_numpy() if "XW_mm" in df else np.zeros(len(df))
|
| 78 |
tm_vals = df["TMRW_mm"].fillna(0).to_numpy() if "TMRW_mm" in df else np.zeros(len(df))
|
| 79 |
df["peak_proxy"] = np.maximum(xw_vals, tm_vals)
|
| 80 |
-
|
| 81 |
if df["peak_proxy"].max() > 0:
|
| 82 |
-
peak_idx
|
| 83 |
peak_when = df.iloc[peak_idx]["Local Time"]
|
| 84 |
-
peak_amt
|
| 85 |
else:
|
| 86 |
peak_when, peak_amt = None, 0.0
|
| 87 |
|
|
@@ -92,12 +94,12 @@ def build_pretty_table(df: pd.DataFrame) -> tuple[str, dict]:
|
|
| 92 |
"has_rain": bool(next_rain),
|
| 93 |
}
|
| 94 |
|
| 95 |
-
# --- Grouped
|
| 96 |
rows_html = []
|
| 97 |
rows_html.append(
|
| 98 |
"<thead>"
|
| 99 |
"<tr>"
|
| 100 |
-
"<th rowspan='2'>Local Time</th>"
|
| 101 |
"<th class='xw-col' colspan='3'>Xweather</th>"
|
| 102 |
"<th class='tm-col' colspan='3'>Tomorrow.io</th>"
|
| 103 |
"</tr>"
|
|
@@ -118,7 +120,7 @@ def build_pretty_table(df: pd.DataFrame) -> tuple[str, dict]:
|
|
| 118 |
body = []
|
| 119 |
for i, r in df.iterrows():
|
| 120 |
alert = rain_flag.iloc[i]
|
| 121 |
-
tr_style = "background:
|
| 122 |
cells = [
|
| 123 |
f"<td>{escape(str(r.get('Local Time','')))}</td>",
|
| 124 |
f"<td class='xw-cell'>{_wx_emoji(r.get('XW_Weather'))}</td>",
|
|
@@ -130,20 +132,51 @@ def build_pretty_table(df: pd.DataFrame) -> tuple[str, dict]:
|
|
| 130 |
]
|
| 131 |
body.append(f"<tr style='{tr_style}'>" + "".join(cells) + "</tr>")
|
| 132 |
|
|
|
|
| 133 |
table_css = """
|
| 134 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
.wx-table { width: 100%; border-collapse: collapse; font-family: ui-sans-serif, system-ui, -apple-system; }
|
| 136 |
-
.wx-table th, .wx-table td {
|
|
|
|
|
|
|
|
|
|
| 137 |
.wx-table thead th { position: sticky; top: 0; z-index: 1; }
|
| 138 |
-
.wx-table th.
|
| 139 |
-
.wx-table th.
|
| 140 |
-
.wx-table th
|
| 141 |
-
.xw-cell { background:
|
| 142 |
-
.tm-cell { background:
|
|
|
|
| 143 |
.wx-chips { display:flex; gap:8px; flex-wrap: wrap; margin: 10px 0 6px; }
|
| 144 |
-
.chip { padding:6px 10px; border-radius: 999px; background:
|
| 145 |
-
.chip.good {
|
| 146 |
-
.chip.warn {
|
| 147 |
</style>
|
| 148 |
"""
|
| 149 |
|
|
@@ -157,13 +190,15 @@ def build_pretty_table(df: pd.DataFrame) -> tuple[str, dict]:
|
|
| 157 |
|
| 158 |
table_html = f"""
|
| 159 |
{table_css}
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
| 167 |
"""
|
| 168 |
return table_html, summary
|
| 169 |
|
|
@@ -182,8 +217,9 @@ TMRW_WEATHER_CODE = {
|
|
| 182 |
1001: "Cloudy", 2000: "Fog", 2100: "Light Fog",
|
| 183 |
4000: "Drizzle", 4001: "Rain", 4200: "Light Rain", 4201: "Heavy Rain",
|
| 184 |
5000: "Snow", 5001: "Flurries", 5100: "Light Snow", 5101: "Heavy Snow",
|
| 185 |
-
6000: "Freezing Drizzle", 6001: "Freezing Rain", 6200: "Light Freezing Rain",
|
| 186 |
-
|
|
|
|
| 187 |
8000: "Thunderstorm"
|
| 188 |
}
|
| 189 |
|
|
@@ -240,7 +276,7 @@ def fetch_tomorrow_hourly(latlon: str, hours: int = 24):
|
|
| 240 |
"TMRW_Weather": TMRW_WEATHER_CODE.get(code, str(code) if code is not None else None),
|
| 241 |
"TMRW_Humidity_%": v.get("humidity"),
|
| 242 |
"TMRW_RAIN_PROB_%": v.get("precipitationProbability"),
|
| 243 |
-
"TMRW_PrecipRate_mmph": v.get("
|
| 244 |
"TMRW_Visibility_km": v.get("visibility"),
|
| 245 |
"TMRW_Cloud_%": v.get("cloudCover"),
|
| 246 |
})
|
|
@@ -257,7 +293,7 @@ def build_side_by_side(location: str):
|
|
| 257 |
local_tz = ZoneInfo("Asia/Singapore") # UTC+8
|
| 258 |
merged["Local Time"] = (
|
| 259 |
pd.to_datetime(merged["time_hour_utc"])
|
| 260 |
-
.dt.tz_convert(local_tz)
|
| 261 |
.dt.strftime("%Y-%m-%d %H:%M %Z")
|
| 262 |
)
|
| 263 |
except Exception:
|
|
|
|
| 21 |
"""Safely format weather text with emoji icons"""
|
| 22 |
if txt is None or (isinstance(txt, float) and np.isnan(txt)):
|
| 23 |
return "β"
|
| 24 |
+
txt = str(txt) # ensure string
|
| 25 |
t = txt.lower()
|
| 26 |
if "thunder" in t: return "βοΈ " + txt
|
| 27 |
if "shower" in t or "rain" in t or "drizzle" in t: return "π§οΈ " + txt
|
|
|
|
| 30 |
if "clear" in t or "sun" in t: return "βοΈ " + txt
|
| 31 |
return txt
|
| 32 |
|
|
|
|
| 33 |
def _fmt(x, nd=1):
|
| 34 |
if x is None or (isinstance(x, float) and np.isnan(x)): return "β"
|
| 35 |
if isinstance(x, (int, np.integer)): return f"{x:d}"
|
|
|
|
| 37 |
return escape(str(x))
|
| 38 |
|
| 39 |
def _heat(value, vmax, hue="blue"):
|
| 40 |
+
"""Inline, subtle heat-tint that works in light/dark."""
|
| 41 |
try:
|
| 42 |
v = float(value or 0.0)
|
| 43 |
except:
|
| 44 |
v = 0.0
|
| 45 |
pct = max(0.0, min(1.0, v / max(vmax, 1e-9)))
|
| 46 |
if hue == "blue":
|
| 47 |
+
# blue overlay
|
| 48 |
+
return f"background: rgba(59, 130, 246, {0.18*pct});"
|
| 49 |
if hue == "green":
|
| 50 |
+
# green overlay
|
| 51 |
+
return f"background: rgba(34, 197, 94, {0.18*pct});"
|
| 52 |
return ""
|
| 53 |
|
| 54 |
def build_pretty_table(df: pd.DataFrame) -> tuple[str, dict]:
|
|
|
|
| 56 |
return "<p>No data.</p>", {}
|
| 57 |
|
| 58 |
df = df.copy()
|
| 59 |
+
df["XW_mm"] = pd.to_numeric(df.get("XW_Precip_mm"), errors="coerce")
|
| 60 |
df["TMRW_mm"] = pd.to_numeric(df.get("TMRW_PrecipRate_mmph"), errors="coerce")
|
| 61 |
+
df["XW_PoP"] = pd.to_numeric(df.get("XW_RAIN_PROB_%"), errors="coerce")
|
| 62 |
+
df["TMRW_PoP"]= pd.to_numeric(df.get("TMRW_RAIN_PROB_%"), errors="coerce")
|
| 63 |
|
| 64 |
+
# Rain alert rows
|
| 65 |
rain_flag = (
|
| 66 |
((df["XW_mm"] >= 1.0) | (df["TMRW_mm"] >= 1.0)) |
|
| 67 |
((df["XW_PoP"] >= 70) | (df["TMRW_PoP"] >= 70))
|
| 68 |
)
|
| 69 |
|
| 70 |
+
# Next rain (either provider hits threshold first)
|
| 71 |
def _first_rain_row():
|
| 72 |
for _, row in df.iterrows():
|
| 73 |
if (row.get("XW_mm", 0) >= 1) or (row.get("TMRW_mm", 0) >= 1) or \
|
|
|
|
| 76 |
return None
|
| 77 |
next_rain = _first_rain_row()
|
| 78 |
|
| 79 |
+
# Peak rain (max of providers)
|
| 80 |
xw_vals = df["XW_mm"].fillna(0).to_numpy() if "XW_mm" in df else np.zeros(len(df))
|
| 81 |
tm_vals = df["TMRW_mm"].fillna(0).to_numpy() if "TMRW_mm" in df else np.zeros(len(df))
|
| 82 |
df["peak_proxy"] = np.maximum(xw_vals, tm_vals)
|
|
|
|
| 83 |
if df["peak_proxy"].max() > 0:
|
| 84 |
+
peak_idx = int(df["peak_proxy"].idxmax())
|
| 85 |
peak_when = df.iloc[peak_idx]["Local Time"]
|
| 86 |
+
peak_amt = float(df.iloc[peak_idx]["peak_proxy"])
|
| 87 |
else:
|
| 88 |
peak_when, peak_amt = None, 0.0
|
| 89 |
|
|
|
|
| 94 |
"has_rain": bool(next_rain),
|
| 95 |
}
|
| 96 |
|
| 97 |
+
# --- Grouped headers (2 rows) ---
|
| 98 |
rows_html = []
|
| 99 |
rows_html.append(
|
| 100 |
"<thead>"
|
| 101 |
"<tr>"
|
| 102 |
+
"<th rowspan='2' class='local-col'>Local Time</th>"
|
| 103 |
"<th class='xw-col' colspan='3'>Xweather</th>"
|
| 104 |
"<th class='tm-col' colspan='3'>Tomorrow.io</th>"
|
| 105 |
"</tr>"
|
|
|
|
| 120 |
body = []
|
| 121 |
for i, r in df.iterrows():
|
| 122 |
alert = rain_flag.iloc[i]
|
| 123 |
+
tr_style = "background: var(--alert-bg);" if alert else ""
|
| 124 |
cells = [
|
| 125 |
f"<td>{escape(str(r.get('Local Time','')))}</td>",
|
| 126 |
f"<td class='xw-cell'>{_wx_emoji(r.get('XW_Weather'))}</td>",
|
|
|
|
| 132 |
]
|
| 133 |
body.append(f"<tr style='{tr_style}'>" + "".join(cells) + "</tr>")
|
| 134 |
|
| 135 |
+
# --- Adaptive (light/dark) CSS using CSS variables ---
|
| 136 |
table_css = """
|
| 137 |
<style>
|
| 138 |
+
:root {
|
| 139 |
+
--bg: #ffffff;
|
| 140 |
+
--fg: #111111;
|
| 141 |
+
--muted: #fafafa;
|
| 142 |
+
--border: #e5e7eb;
|
| 143 |
+
--xw-head: #cfe2ff; /* header blue */
|
| 144 |
+
--tm-head: #d4edda; /* header green */
|
| 145 |
+
--xw-cell: #f8fbff; /* body blue tint */
|
| 146 |
+
--tm-cell: #f9fff9; /* body green tint */
|
| 147 |
+
--alert-bg: rgba(255, 193, 7, 0.12); /* amber */
|
| 148 |
+
}
|
| 149 |
+
@media (prefers-color-scheme: dark) {
|
| 150 |
+
:root {
|
| 151 |
+
--bg: #0b1220;
|
| 152 |
+
--fg: #e5e7eb;
|
| 153 |
+
--muted: #131c2b;
|
| 154 |
+
--border: #243042;
|
| 155 |
+
--xw-head: #10335f;
|
| 156 |
+
--tm-head: #0f3b26;
|
| 157 |
+
--xw-cell: #0f213e;
|
| 158 |
+
--tm-cell: #0f2a1a;
|
| 159 |
+
--alert-bg: rgba(255, 193, 7, 0.18);
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.wx-scope { color: var(--fg); background: transparent; }
|
| 164 |
.wx-table { width: 100%; border-collapse: collapse; font-family: ui-sans-serif, system-ui, -apple-system; }
|
| 165 |
+
.wx-table th, .wx-table td {
|
| 166 |
+
padding: 10px 8px; border-bottom: 1px solid var(--border);
|
| 167 |
+
font-size: 14px; vertical-align: middle; text-align: center; color: var(--fg);
|
| 168 |
+
}
|
| 169 |
.wx-table thead th { position: sticky; top: 0; z-index: 1; }
|
| 170 |
+
.wx-table th.local-col { background: var(--muted); }
|
| 171 |
+
.wx-table th.xw-col { background: var(--xw-head); }
|
| 172 |
+
.wx-table th.tm-col { background: var(--tm-head); }
|
| 173 |
+
.xw-cell { background: var(--xw-cell); }
|
| 174 |
+
.tm-cell { background: var(--tm-cell); }
|
| 175 |
+
|
| 176 |
.wx-chips { display:flex; gap:8px; flex-wrap: wrap; margin: 10px 0 6px; }
|
| 177 |
+
.chip { padding:6px 10px; border-radius: 999px; background: var(--muted); border:1px solid var(--border); font-size: 13px; color: var(--fg); }
|
| 178 |
+
.chip.good { box-shadow: inset 0 0 0 9999px rgba(34,197,94,0.12); }
|
| 179 |
+
.chip.warn { box-shadow: inset 0 0 0 9999px rgba(255,193,7,0.12); }
|
| 180 |
</style>
|
| 181 |
"""
|
| 182 |
|
|
|
|
| 190 |
|
| 191 |
table_html = f"""
|
| 192 |
{table_css}
|
| 193 |
+
<div class="wx-scope">
|
| 194 |
+
{header_html}
|
| 195 |
+
<table class="wx-table">
|
| 196 |
+
{''.join(rows_html)}
|
| 197 |
+
<tbody>
|
| 198 |
+
{''.join(body)}
|
| 199 |
+
</tbody>
|
| 200 |
+
</table>
|
| 201 |
+
</div>
|
| 202 |
"""
|
| 203 |
return table_html, summary
|
| 204 |
|
|
|
|
| 217 |
1001: "Cloudy", 2000: "Fog", 2100: "Light Fog",
|
| 218 |
4000: "Drizzle", 4001: "Rain", 4200: "Light Rain", 4201: "Heavy Rain",
|
| 219 |
5000: "Snow", 5001: "Flurries", 5100: "Light Snow", 5101: "Heavy Snow",
|
| 220 |
+
6000: "Freezing Drizzle", 6001: "Freezing Rain", 6200: "Light Freezing Rain",
|
| 221 |
+
6201: "Heavy Freezing Rain", 7000: "Ice Pellets",
|
| 222 |
+
7101: "Heavy Ice Pellets", 7102: "Light Ice Pellets",
|
| 223 |
8000: "Thunderstorm"
|
| 224 |
}
|
| 225 |
|
|
|
|
| 276 |
"TMRW_Weather": TMRW_WEATHER_CODE.get(code, str(code) if code is not None else None),
|
| 277 |
"TMRW_Humidity_%": v.get("humidity"),
|
| 278 |
"TMRW_RAIN_PROB_%": v.get("precipitationProbability"),
|
| 279 |
+
"TMRW_PrecipRate_mmph": v.get("precipitationIntensity"), # β
correct field
|
| 280 |
"TMRW_Visibility_km": v.get("visibility"),
|
| 281 |
"TMRW_Cloud_%": v.get("cloudCover"),
|
| 282 |
})
|
|
|
|
| 293 |
local_tz = ZoneInfo("Asia/Singapore") # UTC+8
|
| 294 |
merged["Local Time"] = (
|
| 295 |
pd.to_datetime(merged["time_hour_utc"])
|
| 296 |
+
.dt.tz_convert(local_tz)
|
| 297 |
.dt.strftime("%Y-%m-%d %H:%M %Z")
|
| 298 |
)
|
| 299 |
except Exception:
|