A Val Town app that visualizes Helium Mobile hotspot growth in the DC Metro area over time, featuring an interactive Leaflet map with H3-based clustering (rendered as scaled circles) and timeline scrubbing.
Platform : Val Town (Deno runtime)
Backend : Hono HTTP framework
Database : Val Town SQLite
Frontend : React + deck.gl (WebGL)
Clustering : Uber H3 (resolution 8) for grouping
Map Tiles : CartoDB dark-matter (via TileLayer)
Styling : Helium-inspired dark theme
index.ts - Main Hono server with API routes
backend/db.ts - SQLite schema and queries
backend/heliumApi.ts - Helium Entity API client
backend/scraper.ts - Backfill and daily scrape logic
cron.ts - Daily scraper cron job (6 AM UTC)
frontend/components/App.tsx - Main React component with map, timeline, and clustering
frontend/utils/animalHash.ts - Animal name generator (angry-purple-tiger algorithm)
frontend/style.css - Helium-inspired dark theme styles
Push changes: ~/.deno/bin/vt push
Open in browser: ~/.deno/bin/vt browse
Configure cron schedule in Val Town web interface
Using ScatterplotLayer with radiusUnits: "pixels"
Circles scaled by count using sqrt easing curve : radius = 8 + 27 * (sqrt(count-1) / sqrt(69))
Min radius: 8px (count=1), Max radius: 35px (count=70)
Easing makes 1→2 dramatic but 67→70 nearly imperceptible
Built-in transitions: transitions: { getRadius: { duration: 150 }, getFillColor: { duration: 150 } }
TileLayer for CartoDB dark basemap tiles
Stroke: 1px width, 0.15 opacity (white)
Animal Names (animalHash.ts)
Uses exact angry-purple-tiger algorithm with blueimp-md5
Critical: ADJECTIVES array must include duplicate "skinny" at index 85 (after "shallow")
Display names in Title Case (e.g., "Angry Purple Tiger"), not kebab-case
Histogram bars must have gap: 0 to align with slider (gaps accumulate across ~1000 bars)
Histogram height: 40px desktop, 30px mobile
Histogram capped at 10 new hotspots to prevent outliers from flattening
Playback at 15ms interval (~67 fps)
When at end of timeline and user presses play, reset to beginning
Use height: 100dvh (dynamic viewport height) instead of 100vh
Add viewport-fit=cover to viewport meta tag
Use env(safe-area-inset-bottom) for timeline padding
Visual Design (Helium-inspired)
Dark theme with --bg-primary: #04081b
Accent colors: cyan (#4FC3F7), green (#00d97d), purple (#5e25fd)
Avoid bold fonts - use font-weight 400-500
Circle fill opacity: 0.5 for established (grey), 0.7 for new/recent
Map zoom control hidden (zoomControl: false)
Color Coding (Hotspot Age)
Green (#2ecc71): New (< 7 days)
Blue (#3498db): Recent (< 30 days)
Grey (#a0a0a0): Established (30+ days)
Mobile Responsive (max-width: 768px)
Timeline padding: 10px 12px with safe-area-inset-bottom
Histogram height: 30px
Play button: 36x36px
Legend hidden on mobile
GET /api/hotspots - All active hotspots
GET /api/snapshots - Daily snapshot stats
GET /api/stats - Current count and latest snapshot
POST /api/admin/backfill - Trigger data backfill
POST /api/admin/scrape - Trigger daily scrape
const DC_BOUNDS = {
minLat : 38.5 , maxLat : 39.3 ,
minLong : -77.5 , maxLong : -76.7
};