A Digital Twin
of the Federation
Sixteen states. 160 districts. 32.4 million people. Every public signal about Malaysia that can be read in real time, drawn on one map, with the slow layers underneath. Built by Dr Non Arkaraprasertkul as a gift to depa (Digital Economy Promotion Agency of Thailand) and the ASEAN Smart Cities Network (ASCN) 2026.
01Dr Non Arkaraprasertkul
Most important thing first: this is a gift.
Dr Non is a Thai urbanist who works on city systems — flood, mobility, air, fire, water. He has shipped dashboards for Bangkok floods (FloodDash), Chiang Mai (CNX), and Greater Kuching under the ASCN. He also keeps two personal projects — nsp-prototype (a podcast + soccer-life essay under his own name) and pirchchat (Burma monitoring).
The Malaysia project extends three boards he has already shipped. Where FloodDash ends at a basin, CNX ends at a city, and the Greater Kuching IOC ends at a state, this one ends at the federation.
"A good dashboard tells you what the room is doing right now. A great dashboard tells you what the room cannot tell you. This one does both, in three languages."
— Dr Non Arkaraprasertkul, project notes, September 2026
02This week, in data
What the dashboard has measured in the past seven days, with sources. Each row is live in the running app; the numbers below are a snapshot of the rolling week ending today, 25 September 2026.
What the news has been
The press pulse aggregates Google News in EN / BM / 中文 on a 10-minute interval. As of today, the top three regional stories are about the southwest monsoon withdrawal across the Malay Peninsula, the East Coast Rail Link (ECRL) reaching the Kota Bharu integrated terminal, and the second-quarter GDP release from Bank Negara (5.2% YoY, vs. 5.0% consensus).
If a FACEBOOK_PAGE_ID and FACEBOOK_PAGE_ACCESS_TOKEN are configured on the server, the same press pulse now pulls the latest posts from a designated Facebook Page through the Graph API v18.0 — see § 06. Without credentials, the page stays empty and the Google News panel remains the sole source of press.
03Mathematics
Five small maths. Nothing exotic. The dashboard earns its keep by not lying when the maths is uncertain.
3.1 Great-circle distance
Every reach-analysis pixel walks to the nearest mapped facility with the standard Haversine formula over WGS-84.
a = sin²(Δφ / 2) + cos(φ₁) · cos(φ₂) · sin²(Δλ / 2) c = 2 · atan2(√a, √(1−a)) d = R · c R = 6,371 km
Real journeys by road or river are longer. The headline always says "straight line".
3.2 Reach analysis
For every ~1 km² cell where Meta HRSL says n people live:
for each facility f ∈
{hospitals, clinics, schools, police, fire}:
km[c, f] = haversine(c, f)
nearest[c] = argmin_f km[c, f]
if km[c, nearest_hospital] > 20:
people_>20km += n
total_>20km = Σ people_>20km
3.3 Freshness grading
Each feed carries an observation time, not a fetch time. Age against cadence decides status:
grade(f) = loading if !f.fetched error if f.error nokey if error matches /no .*key/ stale if f.obs > now + 5m stale if age > 4 · cadence delayed if age > cadence live otherwise
A 200 OK with yesterday's numbers is not "live".
3.4 Trilingual interpolation
Bahasa forecast strings are matched against a small phrase table in feeds.js and replaced with EN or 中文 equivalents. The original phrase always backs the rain-icon lookup, so the emoji never disagrees with the displayed text.
3.5 Air quality
PM2.5 in 21 state capitals is the Open-Meteo CAMS reanalysis at 0.4° — a model, not a measurement. Bands follow the US EPA cut-points (12 / 35 / 55 / 150 µg/m³).
3.6 Reach map (visual)
04Data pipeline
Every feed runs through the same shape: fetch → normalise → redraw → grade → notify. Polling lives in core.js → poll(id, fn). When the tab is hidden, polling pauses; when it returns, every feed whose last fetch is older than its interval re-runs.
fetch(url, { headers, signal: timeout(10s) })
↓
JSON.parse → feed-specific normalise()
↓
D.{feed} → layer redraw (Leaflet)
↓
mark(id, ok, newestObservationTime)
↓
grade(f) → status: live | delayed | stale | error | nokey | loading
↓
regrade() → re-render verdict, KPIs, tab, status pill
Every feed, its cadence, its freshness window
| Feed | Endpoint | ↻ | ok ≤ |
|---|---|---|---|
| JPS river gauges | publicinfobanjir.water.gov.my | 120 s | 45 m |
| MET warnings | api.data.gov.my | 300 s | 15 m |
| MET forecast | api.data.gov.my | 30 m | 36 h |
| USGS + MET earthquakes | earthquake.usgs.gov | 300 s | 60 m |
| GTFS-Realtime (Prasarana, myBAS, KTMB) | developer.data.gov.my | 20 s | 5 m |
| Aircraft (adsb.lol / OpenSky) | adsb.lol | 30 s | 5 m |
| AIS ships (Axiom Overwatch) | axiomoverwatch.io | 300 s | 45 m |
| Airport arrivals | opensky-network.org | 1 h | 36 h |
| Open-Meteo air quality (CAMS) | open-meteo.com | 600 s | 2 h |
| RainViewer radar | rainviewer.com | 300 s | 30 m |
| NASA FIRMS fire hotspots | firms.modaps.eosdis.nasa.gov | 30 m | 24 h |
| Blitzortung lightning (PocketWorld) | pocketworld.org | 60 s | 5 m |
| Bank Negara rates | apikijangportal.bnm.gov.my | 30 m | 4 d |
| OpenDOSM CPI | open.dosm.gov.my | 6 h | 70 d |
| JAKIM e-Solat prayer times | e-solat.gov.my | 1 h | 36 h |
| Google News (EN / BM / 中文) | news.google.com | 10 m | 24 h |
| Facebook Page posts | graph.facebook.com | — | — |
Three feeds are baked (one-time, static): reach analysis, Census 2020 + HIES 2019, and the boundaries. The Facebook Page endpoint is enabled only when both FACEBOOK_PAGE_ID and FACEBOOK_PAGE_ACCESS_TOKEN are set on the server; otherwise it returns an empty array.
05Facebook integration
The press pulse now reaches into Facebook's Graph API v18.0 through /api/facebook. The endpoint returns the most recent 20 posts from a configured page: message, created_time, permalink_url, full_picture, plus reaction, comment, and share counts.
Configuration is two environment variables on the Cloudflare Pages project:
FACEBOOK_PAGE_ID = 123456789 FACEBOOK_PAGE_ACCESS_TOKEN = EAAxxxxxx...
The token is a long-lived Page access token, generated from a Facebook App with pages_read_engagement + pages_show_list scopes. Without it the endpoint returns { posts: [], note: "..." } and the dashboard continues to use Google News alone.
On the front-end, Facebook posts flow into the same rail tab as the news pulse. Source attribution shows Facebook · <page-name>. The cache is 5 minutes at the edge.
Example wire shape
GET /api/facebook
200 OK
{
"source": "Facebook Graph API",
"configured": true,
"posts": [
{
"id": "123_456",
"message": "Today at 14:32 MYT...",
"time": "2026-09-25T06:32:00+0000",
"url": "https://facebook.com/...",
"image": "https://scontent...",
"reactions": 432,
"comments": 41,
"shares": 18
}
]
}
Failure mode
If the token has expired or the page is unpublished, the relay returns 502 with error set. The press tab hides that source and shows the remaining feeds.
06Safety & limitations
"This board is decision support, not an official warning."
The dashboard never claims it can establish that an area is safe. If Coverage is incomplete appears, the headline is honest about what it does not know. A red banner names the feed that has gone stale and the time since it last answered.
What the dashboard does not claim
- Hospital locations are reference data. They do not confirm opening hours, available beds, or an accessible route.
- Reach distances are straight line. Real journeys by road or river are longer.
- Census 2020 and HIES 2019 are historical.
- Air quality is a model, not a measurement.
- Ship "declared destinations" are what the ship transmits, not where it actually goes.
- Rain radar is 10-minute frames.
- Lightning shows the last 15 minutes of strikes that crossed Malaysia's bounding box.
- Facebook posts are public Page content; we do not fetch comments from posts older than 24 hours or DMs.
What it does claim
- If a river gauge reads DANGER within the last 45 minutes, that is the operator-reported category for that station.
- If a MET warning is in valid_to ≥ now, it is the current authoritative text from MET Malaysia.
- If the verdict says Not enough data, the upstream is broken — not the dashboard.
07Credits
Built by Dr Non Arkaraprasertkul as a gift to depa (Digital Economy Promotion Agency of Thailand) and the ASEAN Smart Cities Network (ASCN) 2026. Fonts: Plus Jakarta Sans by Tokotype (Jakarta); Newsreader by Production Type; Noto Sans SC; IBM Plex Mono. Independent demonstration. Not an official product of the Government of Malaysia.
Dibina oleh Dr Non Arkaraprasertkul sebagai hadiah kepada depa (Agensi Galakan Ekonomi Digital Thailand) dan Rangkaian Bandar Pintar ASEAN (ASCN) 2026. Fon: Plus Jakarta Sans oleh Tokotype (Jakarta); Newsreader oleh Production Type; Noto Sans SC; IBM Plex Mono. Demonstrasi bebas. Bukan produk rasmi Kerajaan Malaysia.
由 Dr Non Arkaraprasertkul 制作,作为赠予泰国 depa(数字经济促进局)及 东盟智慧城市网络 (ASCN) 2026 的礼物。字体:Plus Jakarta Sans(Tokotype,雅加达);Newsreader(Production Type);Noto Sans SC;IBM Plex Mono。独立演示项目,并非马来西亚政府官方产品。
Data attribution
- Weather warnings, forecast, earthquakes: MET Malaysia via api.data.gov.my.
- River gauges: Jabatan Pengairan dan Saliran (JPS) / DID via publicinfobanjir.water.gov.my.
- Census 2020 + HIES 2019 + boundaries: DOSM via github.com/dosm-malaysia/data-open (CC BY 4.0).
- Population grid: Meta HRSL (CC BY 4.0).
- Facilities: Overture Maps 2026-08 (CC BY 4.0).
- Terrain: AWS Terrarium tiles, derived from Copernicus / SRTM.
- Waterways: Overture Maps 2026-08 (CC BY 4.0).
- Buses + trains: Prasarana Malaysia, myBAS Johor, KTMB via GTFS-Realtime.
- Aircraft: adsb.lol and OpenSky Network.
- Arrivals: OpenSky Network or AirLabs.
- Ships: Axiom Overwatch (CC-BY 4.0) + VesselFinder.
- Fire hotspots: NASA FIRMS (VIIRS, public).
- Satellite: NASA GIBS, JMA / NASA GIBS (Himawari-9), JRC, Copernicus Sentinel-1/-2, ECMWF via Open-Meteo.
- Lightning: Blitzortung via PocketWorld.
- Rates: Bank Negara Malaysia.
- CPI, unemployment: OpenDOSM.
- Prayer times: JAKIM e-Solat.
- News: Google News RSS (EN / BM / 中文) + Facebook Graph API v18.0 (optional, configured).
License
Application code: MIT. Fonts: OFL (Plus Jakarta Sans, Newsreader, Noto Sans SC, IBM Plex Mono). Data layers: see the CC BY 4.0 / public domain notices above. The Malaysia state outline comes from DOSM's data-open repository. This repository is private.
— end —