Bryan Hamilton / projects / next-show-radar

bryanhamilton.info / open source / next-show-radar

next-show-radar "No" is a first-class answer.

The tour map from The DJ Calendar, extracted as a standalone widget. Allow location and it shows you the shows within 200 miles. Decline, and you still get a fully working map of every show. No answer to the permission prompt ever breaks it.

  • Consent-first geolocation
  • Haversine distance
  • Leaflet, no API key
  • MIT license
In brief: next-show-radar is a JavaScript and Leaflet tour-map widget that filters upcoming shows by distance after a visitor grants geolocation. If location is denied or unavailable, it still displays every supplied show and announces that fallback accessibly.

Anyma's next shows

Real upcoming dates from The DJ Calendar

or pretend you're in:

Loading the map…

Showing all 5 upcoming shows.Nothing is asked until you press "Use my location".

  1. Oct 17Anyma Presents ÆDEN - SydneyThe Domain · Sydney, NSWTickets →
  2. Oct 31Anyma presents ÆDEN - AthensAthens · Athens, GreeceTickets →
  3. Nov 21Anyma Presents ÆDEN - MumbaiMumbai · Mumbai, IndiaTickets →
  4. Dec 4Anyma Presents ÆDEN - Abu DhabiYas Gateway Park North · Abu Dhabi, UAETickets →
  5. Dec 12Anyma Presents ÆDEN - ParisNanterre · Paris, FranceTickets →

Pins use city-level coordinates. Your real location (if you share it) stays in your browser: it's never sent anywhere.

What actually happens

When a site asks for your location, the browser shows a small prompt: "this site wants to know your location. Allow or Block." That prompt is the whole game, and the radar is built around both answers.

Allow, and the browser hands the page a latitude and longitude. The radar drops a red "You" dot there, measures the real distance to every show, and zooms to fit the ones within 200 miles. The caption under the map says how many there are, so a fan instantly knows their favorite DJ is playing twenty minutes away.

Block, and the browser hands the page nothing at all. This is where most map widgets fail: the code crashes, or the map turns into a blank gray rectangle. The radar does neither. It quietly falls back to the experience it always had: every show on a world map, zoomed to fit, with a calm caption saying location isn't available. Plenty of people tap Block, and they're right to be careful. They still get a map that works.

The demo above asks only when you press the button. The "pretend" buttons show the same logic without sharing anything.

Every answer has a designed outcome

Allowed, shows nearby
A red "You" marker, the map fitted around you and every show inside the radius, and an announcement like "2 shows within 200 miles". Try Dubai above.
Allowed, nothing nearby
The map centers on the visitor's own neighborhood, so it still feels personal. Try Rochelle Park: Anyma's tour doesn't come near New Jersey.
Declined or unavailable
The full world view of every show, plus "Location not available, showing all shows". Nothing breaks and nothing is hidden.
Asked before
The first answer is remembered, so a visitor who said no is never nagged with the prompt again.

Real distance on a round planet

"Within 200 miles" has to mean miles a person would actually travel, not a box drawn on a flat map. The radar uses the haversine formula, which measures the great-circle distance between two points on a sphere:

function milesBetween(a, b) {
  const R = 3958.8;                                 // Earth's radius in miles
  const rad = d => d * Math.PI / 180;
  const dLat = rad(b.lat - a.lat), dLng = rad(b.lng - a.lng);
  const h = Math.sin(dLat / 2) ** 2 +
            Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLng / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(h));
}

The standard formula, as used in the demo. The production code is in src/ on GitHub.

The data contract

One JSON array, rendered into the page by the server or set at runtime. No fetch, no API keys, no backend:

[
  {
    "lat": 40.759,
    "lng": -73.9845,
    "title": "Alan Walker",
    "venue": "Marquee New York",
    "city": "New York City, NY",
    "date": "Jun 6",
    "url": "https://example.com/tickets"
  }
]

Points without numeric coordinates are silently left off the map. A show that never got geocoded must never become a pin at 0,0 in the middle of the South Atlantic.

Why the default map tiles changed

For years the widget used CARTO's free "Dark Matter" map tiles. Then in 2026 CARTO began requiring API keys for keyless traffic, and maps that relied on them started showing error tiles that read "API key required." A widget whose default map can silently break is a widget nobody can trust.

So the default is now Esri's dark gray canvas, which works today with no account (free with attribution, which the widget supplies), and the tile source is a single setting. When a provider changes its policy, you change one attribute, not your code. The demo above runs on that same Esri default.

Accessible, and race-safe

The caption under the map is a real role="status" live region, and it's visible: sighted visitors and screen-reader users get the same sentence for every outcome. With prefers-reduced-motion on, the map skips its pan-and-zoom animations and simply arrives where it means to be.

The boot is guarded against double-initialization and waits for Leaflet before rendering, announcing clearly if the library never arrives. That's not hypothetical: the production loader once hit a bug where Leaflet was injected twice and destroyed an already-initialized map. Its sibling project, sri-lazy-loader, exists to load Leaflet exactly once, verified with Subresource Integrity.

Use it in your project

<link rel="stylesheet" href="leaflet.css">
<link rel="stylesheet" href="src/radar.css">
<script src="src/radar.js" defer></script>

<div data-radar
     data-radar-points='[{"lat":40.759,"lng":-73.9845,"title":"Alan Walker","venue":"Marquee New York","city":"New York City, NY","date":"Jun 6","url":"https://..."}]'>
</div>
SettingDefaultAttribute
Nearby radius (miles)200data-radar-radius
Zoom when nothing is nearby8data-radar-fallback-zoom
Pin color#00d2ffdata-radar-color
Map tilesEsri Dark Graydata-radar-tiles
Tiles' maximum zoom16data-radar-max-zoom

From the repo's README. Leaflet 1.9+ is the one peer dependency. Geolocation needs HTTPS (or localhost).

Frequently asked questions

What happens if a visitor declines location access?

The map still works: it shows all supplied show pins in a global view and announces that location is unavailable instead of rendering an empty map.

How does next-show-radar decide which shows are nearby?

After location is granted, it calculates distances with the Haversine formula and fits shows within the default 200-mile radius. The radius is configurable.

What happens to shows without coordinates?

Points without numeric latitude and longitude are omitted, so an un-geocoded show cannot accidentally appear at 0,0.

Does the widget repeatedly ask for location?

No. The README describes caching the first geolocation result so a visitor is not prompted again after they have answered.

Want features that respect your users?

Bryan Hamilton is a full-stack web engineer with 25+ years in PHP and JavaScript, available immediately for remote, hybrid or on-site roles in the NYC metro area.