Public
Like
dc-hotspot-map
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Viewing readonly version of main branch: v111View latest version
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 routesbackend/db.ts- SQLite schema and queriesbackend/heliumApi.ts- Helium Entity API clientbackend/scraper.ts- Backfill and daily scrape logiccron.ts- Daily scraper cron job (6 AM UTC)frontend/components/App.tsx- Main React component with map, timeline, and clusteringfrontend/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)
- 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: 0to 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 of100vh - Add
viewport-fit=coverto viewport meta tag - Use
env(safe-area-inset-bottom)for timeline padding
- 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)
- Green (#2ecc71): New (< 7 days)
- Blue (#3498db): Recent (< 30 days)
- Grey (#a0a0a0): Established (30+ days)
- Timeline padding: 10px 12px with safe-area-inset-bottom
- Histogram height: 30px
- Play button: 36x36px
- Legend hidden on mobile
GET /api/hotspots- All active hotspotsGET /api/snapshots- Daily snapshot statsGET /api/stats- Current count and latest snapshotPOST /api/admin/backfill- Trigger data backfillPOST /api/admin/scrape- Trigger daily scrape
const DC_BOUNDS = {
minLat: 38.5, maxLat: 39.3,
minLong: -77.5, maxLong: -76.7
};