rickyt commited on
Commit
7464be9
Β·
1 Parent(s): ce62384
Files changed (1) hide show
  1. app.py +71 -35
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 always 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,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
- return f"background: rgba(0, 123, 255, {0.08*pct});"
 
48
  if hue == "green":
49
- return f"background: rgba(0, 200, 83, {0.08*pct});"
 
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"] = pd.to_numeric(df.get("XW_Precip_mm"), errors="coerce")
58
  df["TMRW_mm"] = pd.to_numeric(df.get("TMRW_PrecipRate_mmph"), errors="coerce")
59
- df["XW_PoP"] = pd.to_numeric(df.get("XW_RAIN_PROB_%"), errors="coerce")
60
- df["TMRW_PoP"] = pd.to_numeric(df.get("TMRW_RAIN_PROB_%"), errors="coerce")
61
 
62
- # Rain alert condition
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 + peak 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 = int(df["peak_proxy"].idxmax())
83
  peak_when = df.iloc[peak_idx]["Local Time"]
84
- peak_amt = float(df.iloc[peak_idx]["peak_proxy"])
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 header ---
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: rgba(255,165,0,0.08);" if alert else ""
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 { padding: 10px 8px; border-bottom: 1px solid #eee; font-size: 14px; vertical-align: middle; text-align: center;}
 
 
 
137
  .wx-table thead th { position: sticky; top: 0; z-index: 1; }
138
- .wx-table th.xw-col { background: #cfe2ff; text-align: center;} /* Xweather headers */
139
- .wx-table th.tm-col { background: #d4edda; text-align: center;} /* Tomorrow.io headers */
140
- .wx-table th:first-child { background: #fafafa; }
141
- .xw-cell { background: #f8fbff; }
142
- .tm-cell { background: #f9fff9; }
 
143
  .wx-chips { display:flex; gap:8px; flex-wrap: wrap; margin: 10px 0 6px; }
144
- .chip { padding:6px 10px; border-radius: 999px; background: #f5f5f5; border:1px solid #eee; font-size: 13px; }
145
- .chip.good { background:#e6f4ea; border-color:#ccebd7; }
146
- .chip.warn { background:#fff7e6; border-color:#ffe2b3; }
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
- {header_html}
161
- <table class="wx-table">
162
- {''.join(rows_html)}
163
- <tbody>
164
- {''.join(body)}
165
- </tbody>
166
- </table>
 
 
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", 6201: "Heavy Freezing Rain",
186
- 7000: "Ice Pellets", 7101: "Heavy Ice Pellets", 7102: "Light Ice Pellets",
 
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("rainIntensity"),
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) # βœ… directly convert from UTC
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: