• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
jhiller

jhiller

dc-hotspot-map

Map tracking Helium Hotspot growth around Washington DC
Public
Like
1
dc-hotspot-map
Home
Code
7
backend
3
frontend
6
shared
1
CLAUDE.md
README.md
C
cron.ts
H
index.ts
Connections
Environment variables
Branches
1
Pull requests
Remixes
History
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.
Sign up now
Code
/
CLAUDE.md
Code
/
CLAUDE.md
Search
1/23/2026
Viewing readonly version of main branch: v115
View latest version
CLAUDE.md

DC Hotspot Growth Map

Project Overview

A Val Town app that visualizes Helium Mobile hotspot growth in the DC Metro area over time, featuring an interactive deck.gl map with H3-based clustering (rendered as scaled circles) and timeline scrubbing.

Deployment

  • Live URL: https://jhiller--10367c64f7d511f0990e42dde27851f2.web.val.run

Tech Stack

  • 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

Key Files

  • 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

Val Town CLI

  • Push changes: ~/.deno/bin/vt push
  • Open in browser: ~/.deno/bin/vt browse
  • Configure cron schedule in Val Town web interface

Design Decisions

deck.gl Visualization

  • Load via CDN: <script src="https://unpkg.com/deck.gl@8.9.33/dist.min.js"></script>
  • Access as global deck object (esm.sh imports have luma.gl compatibility issues)
  • Using ScatterplotLayer with radiusUnits: "pixels", stroked: false
  • 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 } }
  • TextLayer for count labels: fontFamily: "Figtree, sans-serif", fontWeight: 300
  • TileLayer for CartoDB dark basemap: https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png

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

Timeline/Histogram

  • Interactive scrubber: Click or drag anywhere on histogram (no separate slider)
  • Histogram bars: flex: 1 1 0, min-width: 0 (allows fractional pixel widths)
  • Critical: min-width: 0 prevents overflow on narrow viewports with many bars
  • Histogram height: 50px desktop, 40px 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
  • Playhead positioning: Center on each bar using calc(12px + (100% - 24px) * ${(currentIdx + 0.5) / snapshots.length})
  • Playhead has transform: translateX(-50%) to center the 2px line on calculated position
  • Touch support: onTouchStart, onTouchMove, onTouchEnd handlers mirror mouse events

iOS Safari Viewport Fix

  • 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
  • Glass pane effect: background: var(--bg-card), backdrop-filter: blur(15px), border: 1px solid var(--border-color), border-radius: 12px
  • Two floating panels only: stats panel (top-left with integrated legend) and timeline (bottom)
  • Play button: white background, optical alignment with padding-left: 3px for triangle (reset to 0 for pause icon)

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: 10px margins, 12px padding
  • Histogram height: 40px with 6px 10px padding
  • Play button: 36x36px
  • Legend integrated into stats panel (always visible)

API Endpoints

  • 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

DC Metro Bounding Box

const DC_BOUNDS = { minLat: 38.5, maxLat: 39.3, minLong: -77.5, maxLong: -76.7 };
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.