rickyt commited on
Commit
7d80fff
·
1 Parent(s): 7b5ada2
Files changed (1) hide show
  1. app.py +65 -199
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- # Gradio app: Xweather vs Tomorrow.io hourly (next 24 hours) side-by-side
3
  # Requirements: pip install gradio requests pandas python-dateutil python-dotenv
4
 
5
  import os
@@ -37,178 +37,86 @@ def _fmt(x, nd=1):
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]:
55
  if df is None or len(df) == 0:
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 \
74
- (row.get("XW_PoP", 0) >= 70) or (row.get("TMRW_PoP", 0) >= 70):
75
- return row.get("Local Time")
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
-
90
- summary = {
91
- "next_rain": next_rain,
92
- "peak_when": peak_when,
93
- "peak_mmph": round(peak_amt, 1),
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>"
106
- "<tr>"
107
- "<th class='xw-col'>Weather</th>"
108
- "<th class='xw-col'>Rain Prob. %</th>"
109
- "<th class='xw-col'>Rain mm</th>"
110
- "<th class='tm-col'>Weather</th>"
111
- "<th class='tm-col'>Rain Prob. %</th>"
112
- "<th class='tm-col'>Rain mm/h</th>"
113
- "</tr>"
114
- "</thead>"
115
  )
116
 
117
- vmax_pop = max(70, np.nanmax([df["XW_PoP"].max(), df["TMRW_PoP"].max()]))
118
- vmax_mm = max(2.0, np.nanmax([df["XW_mm"].max(), df["TMRW_mm"].max()]))
119
-
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>",
127
- f"<td class='xw-cell' style='{_heat(r.get('XW_PoP'), vmax_pop, 'green')}'>{_fmt(r.get('XW_PoP'))}</td>",
128
- f"<td class='xw-cell' style='{_heat(r.get('XW_mm'), vmax_mm, 'blue')}'>{_fmt(r.get('XW_mm'))}</td>",
129
- f"<td class='tm-cell'>{_wx_emoji(r.get('TMRW_Weather'))}</td>",
130
- f"<td class='tm-cell' style='{_heat(r.get('TMRW_PoP'), vmax_pop, 'green')}'>{_fmt(r.get('TMRW_PoP'))}</td>",
131
- f"<td class='tm-cell' style='{_heat(r.get('TMRW_mm'), vmax_mm, 'blue')}'>{_fmt(r.get('TMRW_mm'))}</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
 
183
- chips = []
184
- chips.append(f"<span class='chip {'warn' if summary['has_rain'] else 'good'}'>"
185
- f"{'Next rain: ' + escape(summary['next_rain']) if summary['has_rain'] else 'No rain signal in next 24h'}</span>")
186
- if summary["peak_mmph"] > 0:
187
- chips.append(f"<span class='chip'>Peak ~{summary['peak_mmph']} mm/h at {escape(summary['peak_when'] or '')}</span>")
188
-
189
- header_html = "<div class='wx-chips'>" + "".join(chips) + "</div>"
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
 
205
  # -------------------
206
  # API Configs
207
  # -------------------
208
- XW_BASE_URL = "https://data.api.xweather.com/forecasts/"
209
- CLIENT_ID = os.getenv("XWEATHER_CLIENT_ID") or "BlZV8kShcnDxJ2ugQ3b65"
210
- CLIENT_SECRET = os.getenv("XWEATHER_CLIENT_SECRET") or "JYvA8vAJJqEO6yP5QixQw59V3oUKqO9HHvj7ZI2R"
211
-
212
  TMRW_BASE = "https://api.tomorrow.io/v4/weather/forecast"
213
  TMRW_API_KEY = os.getenv("TOMORROW_API_KEY") or "teKj9Rkys1UzWxKBEs36pAR8paCXnPW6"
214
 
@@ -228,37 +136,6 @@ def _safe_to_timestamp(iso: str):
228
  try: return pd.to_datetime(iso)
229
  except Exception: return None
230
 
231
- # ---------- Xweather ----------
232
- def fetch_xweather_hourly(location: str, limit: int = 24):
233
- url = XW_BASE_URL + requests.utils.quote(location.strip(), safe=",")
234
- params = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "filter": "1hr", "limit": limit}
235
- r = requests.get(url, params=params, timeout=20)
236
- r.raise_for_status()
237
- data = r.json()
238
- recs = data.get("response") if isinstance(data, dict) and "response" in data else data
239
- rec = recs[0] if isinstance(recs, list) and recs else data
240
- periods = rec.get("periods") or []
241
- tz_str = (rec.get("profile") or {}).get("tz", "")
242
- rows = []
243
- for p in periods[:limit]:
244
- iso = p.get("dateTimeISO") or p.get("validTime")
245
- t_utc = _safe_to_timestamp(iso)
246
- if isinstance(t_utc, pd.Timestamp) and t_utc.tzinfo:
247
- t_utc = t_utc.tz_convert("UTC")
248
- rows.append({
249
- "time_utc": t_utc,
250
- "XW_Weather": p.get("weather") or p.get("weatherPrimary"),
251
- "XW_Humidity_%": p.get("humidity"),
252
- "XW_RAIN_PROB_%": p.get("pop"),
253
- "XW_Precip_mm": p.get("precipMM"),
254
- "XW_Visibility_km": p.get("visibilityKM"),
255
- "XW_Cloud_%": p.get("sky"),
256
- "XW_TZ": tz_str,
257
- })
258
- df = pd.DataFrame(rows)
259
- df["time_hour_utc"] = pd.to_datetime(df["time_utc"]).dt.tz_convert("UTC").dt.floor("H")
260
- return df
261
-
262
  # ---------- Tomorrow.io ----------
263
  def fetch_tomorrow_hourly(latlon: str, hours: int = 24):
264
  params = {"location": latlon.strip(), "timesteps": "hourly", "units": "metric", "apikey": TMRW_API_KEY}
@@ -272,52 +149,41 @@ def fetch_tomorrow_hourly(latlon: str, hours: int = 24):
272
  v = h.get("values") or {}
273
  code = v.get("weatherCode")
274
  rows.append({
275
- "time_utc": t_utc.tz_convert("UTC") if isinstance(t_utc, pd.Timestamp) and t_utc.tzinfo else t_utc,
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("rainIntensity"), # ✅ correct field
280
  "TMRW_Visibility_km": v.get("visibility"),
281
  "TMRW_Cloud_%": v.get("cloudCover"),
282
  })
283
  df = pd.DataFrame(rows)
284
- df["time_hour_utc"] = pd.to_datetime(df["time_utc"]).dt.tz_convert("UTC").dt.floor("H")
 
 
 
 
 
285
  return df
286
 
287
- # ---------- Merge ----------
288
- def build_side_by_side(location: str):
289
- xw = fetch_xweather_hourly(location, limit=24)
290
- tm = fetch_tomorrow_hourly(location, hours=24)
291
- merged = pd.merge(xw, tm, on="time_hour_utc", how="outer", sort=True)
292
- try:
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:
300
- merged["Local Time"] = merged["time_hour_utc"].astype(str)
301
- return merged, f"**Location:** {location} • Rows: {len(merged)} • Sources: Xweather + Tomorrow.io (UTC+8)"
302
-
303
  # ---------- Gradio UI ----------
304
  with gr.Blocks(fill_height=True) as demo:
305
- gr.Markdown("## Hourly Forecast — Side by Side (Xweather vs Tomorrow.io)")
306
  with gr.Row():
307
  loc = gr.Textbox(label="Location (lat,lon)", placeholder="e.g., -6.21,106.85", value="0.46876,116.16879")
308
- btn = gr.Button("Compare", variant="primary")
309
 
310
- out_table = gr.HTML(label="Hourly comparison (next 24h)")
311
  out_info = gr.Markdown()
312
  dl_btn = gr.DownloadButton(label="Download CSV", value=None)
313
 
314
  def _run(loc_s):
315
  try:
316
- df, meta = build_side_by_side(loc_s)
317
- html, summary = build_pretty_table(df)
318
- csv_path = "forecast_compare.csv"
319
  df.to_csv(csv_path, index=False)
320
- return html, meta, csv_path
321
  except Exception as ex:
322
  return f"<pre>{escape(str(ex))}</pre>", "", None
323
 
 
1
  # app.py
2
+ # Gradio app: Tomorrow.io hourly (next 24 hours)
3
  # Requirements: pip install gradio requests pandas python-dateutil python-dotenv
4
 
5
  import os
 
37
  return escape(str(x))
38
 
39
  def _heat(value, vmax, hue="blue"):
40
+ """Inline, subtle heat-tint"""
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(59, 130, 246, {0.18*pct});"
48
  if hue == "green":
 
49
  return f"background: rgba(34, 197, 94, {0.18*pct});"
50
  return ""
51
 
52
+ def build_pretty_table(df: pd.DataFrame) -> str:
53
  if df is None or len(df) == 0:
54
+ return "<p>No data.</p>"
55
 
56
+ vmax_pop = max(70, np.nanmax(df["TMRW_RAIN_PROB_%"]))
57
+ vmax_mm = max(2.0, np.nanmax(df["TMRW_PrecipRate_mmph"]))
 
 
 
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  rows_html = []
60
  rows_html.append(
61
+ "<thead><tr>"
62
+ "<th>Local Time</th>"
63
+ "<th>Weather</th>"
64
+ "<th>Rain Prob. %</th>"
65
+ "<th>Rain mm/h</th>"
66
+ "<th>Humidity %</th>"
67
+ "<th>Visibility km</th>"
68
+ "<th>Cloud %</th>"
69
+ "</tr></thead>"
 
 
 
 
 
 
70
  )
71
 
 
 
 
72
  body = []
73
+ for _, r in df.iterrows():
74
+ prob = r.get("TMRW_RAIN_PROB_%") or 0
75
+ rain = r.get("TMRW_PrecipRate_mmph") or 0
76
+
77
+ # ✅ Apply green highlight rule
78
+ row_class = ""
79
+ try:
80
+ if (rain >= 1) or (prob > 50 and rain >= 1):
81
+ row_class = "highlight-green"
82
+ except Exception:
83
+ row_class = ""
84
+
85
  cells = [
86
  f"<td>{escape(str(r.get('Local Time','')))}</td>",
87
+ f"<td>{_wx_emoji(r.get('TMRW_Weather'))}</td>",
88
+ f"<td style='{_heat(prob, vmax_pop, 'green')}'>{_fmt(prob)}</td>",
89
+ f"<td style='{_heat(rain, vmax_mm, 'blue')}'>{_fmt(rain)}</td>",
90
+ f"<td>{_fmt(r.get('TMRW_Humidity_%'))}</td>",
91
+ f"<td>{_fmt(r.get('TMRW_Visibility_km'))}</td>",
92
+ f"<td>{_fmt(r.get('TMRW_Cloud_%'))}</td>",
93
  ]
94
+ body.append(f"<tr class='{row_class}'>" + "".join(cells) + "</tr>")
95
 
96
+ table_html = f"""
 
97
  <style>
98
+ .wx-table {{ width: 100%; border-collapse: collapse; font-family: ui-sans-serif, system-ui; }}
99
+ .wx-table th, .wx-table td {{
100
+ padding: 8px; border-bottom: 1px solid #ccc; text-align: center;
101
+ }}
102
+ /* ✅ Full-row green highlight */
103
+ tr.highlight-green td {{
104
+ background: rgba(34,197,94,0.35) !important;
105
+ }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  </style>
107
+ <table class="wx-table">
108
+ {''.join(rows_html)}
109
+ <tbody>
110
+ {''.join(body)}
111
+ </tbody>
112
+ </table>
113
  """
114
+ return table_html
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  # -------------------
118
  # API Configs
119
  # -------------------
 
 
 
 
120
  TMRW_BASE = "https://api.tomorrow.io/v4/weather/forecast"
121
  TMRW_API_KEY = os.getenv("TOMORROW_API_KEY") or "teKj9Rkys1UzWxKBEs36pAR8paCXnPW6"
122
 
 
136
  try: return pd.to_datetime(iso)
137
  except Exception: return None
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  # ---------- Tomorrow.io ----------
140
  def fetch_tomorrow_hourly(latlon: str, hours: int = 24):
141
  params = {"location": latlon.strip(), "timesteps": "hourly", "units": "metric", "apikey": TMRW_API_KEY}
 
149
  v = h.get("values") or {}
150
  code = v.get("weatherCode")
151
  rows.append({
152
+ "time_utc": t_utc,
153
  "TMRW_Weather": TMRW_WEATHER_CODE.get(code, str(code) if code is not None else None),
154
  "TMRW_Humidity_%": v.get("humidity"),
155
  "TMRW_RAIN_PROB_%": v.get("precipitationProbability"),
156
+ "TMRW_PrecipRate_mmph": v.get("rainIntensity"),
157
  "TMRW_Visibility_km": v.get("visibility"),
158
  "TMRW_Cloud_%": v.get("cloudCover"),
159
  })
160
  df = pd.DataFrame(rows)
161
+ local_tz = ZoneInfo("Asia/Singapore")
162
+ df["Local Time"] = (
163
+ pd.to_datetime(df["time_utc"])
164
+ .dt.tz_convert(local_tz)
165
+ .dt.strftime("%Y-%m-%d %H:%M %Z")
166
+ )
167
  return df
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  # ---------- Gradio UI ----------
170
  with gr.Blocks(fill_height=True) as demo:
171
+ gr.Markdown("## Tomorrow.io Hourly Forecast (Next 24h)")
172
  with gr.Row():
173
  loc = gr.Textbox(label="Location (lat,lon)", placeholder="e.g., -6.21,106.85", value="0.46876,116.16879")
174
+ btn = gr.Button("Get Forecast", variant="primary")
175
 
176
+ out_table = gr.HTML(label="Hourly forecast table")
177
  out_info = gr.Markdown()
178
  dl_btn = gr.DownloadButton(label="Download CSV", value=None)
179
 
180
  def _run(loc_s):
181
  try:
182
+ df = fetch_tomorrow_hourly(loc_s, hours=24)
183
+ html = build_pretty_table(df)
184
+ csv_path = "forecast_tomorrow.csv"
185
  df.to_csv(csv_path, index=False)
186
+ return html, f"**Location:** {loc_s} • Rows: {len(df)} • Source: Tomorrow.io", csv_path
187
  except Exception as ex:
188
  return f"<pre>{escape(str(ex))}</pre>", "", None
189