Map Delivery Coverage Zones
An interactive map that generates driving isochrones around a restaurant and displays tiered delivery fee zones with colored overlays.
Delivery apps charge different fees based on how far away you are. That “how far” isn’t straight-line distance — it’s drive time. A restaurant 2 miles away across a bridge might take 20 minutes, while one 4 miles away on a highway takes 10.
This app takes a restaurant address, generates 10/20/30 minute driving isochrones, and overlays them on a map as colored delivery zones with tiered pricing.
What you’ll use
Section titled “What you’ll use”- Geocoding to turn the restaurant address into coordinates
- Isochrones to generate drive-time polygons at 10, 20, and 30 minutes
- MapLibre GL JS to render the zones as colored layers
Key code
Section titled “Key code”Fetching isochrones
Section titled “Fetching isochrones”One API call returns all three contours. Pass comma-separated minute values and get back a GeoJSON FeatureCollection with one polygon per contour:
import Plaza from "@plazafyi/sdk";
const client = new Plaza();
export async function getIsochrones(lat, lng) { return client.v1.isochrone({ lat, lng, contours_minutes: "10,20,30", mode: "auto", });}Zone configuration with tiered fees
Section titled “Zone configuration with tiered fees”Each contour maps to a delivery tier. Define the zones once and use them for both the map layers and the legend:
const ZONES = [ { minutes: 10, color: "rgba(5,150,105,0.35)", stroke: "#059669", fee: "Free" }, { minutes: 20, color: "rgba(245,158,11,0.35)", stroke: "#f59e0b", fee: "$5" }, { minutes: 30, color: "rgba(239,68,68,0.35)", stroke: "#ef4444", fee: "$10" },];Adding GeoJSON layers to MapLibre
Section titled “Adding GeoJSON layers to MapLibre”Sort features largest-first so smaller zones paint on top, creating a bullseye effect. Each zone gets a fill layer and a stroke layer:
// Sort descending so largest zone draws first (smallest on top)const sorted = [...features].sort((a, b) => { return (b.properties?.contour ?? 0) - (a.properties?.contour ?? 0);});
sorted.forEach((feature) => { const contour = feature.properties?.contour ?? 0; const zone = ZONES.find((z) => z.minutes === contour); if (!zone) return;
const id = `zone-${ZONES.indexOf(zone)}`; map.addSource(id, { type: "geojson", data: { type: "FeatureCollection", features: [feature] }, }); map.addLayer({ id: `${id}-fill`, type: "fill", source: id, paint: { "fill-color": zone.color }, }); map.addLayer({ id: `${id}-line`, type: "line", source: id, paint: { "line-color": zone.stroke, "line-width": 2 }, });});How it works
Section titled “How it works”Geocode turns the address into coordinates.
Multi-contour isochrone — instead of three separate requests, pass comma-separated minute values: contours_minutes=10,20,30. Plaza returns a FeatureCollection with one polygon per contour, each representing the area reachable by car within that time.
Layer ordering — add the 30-minute zone first, then 20, then 10. MapLibre draws layers in order, so the smaller zones paint on top.
Fee lookup — in production, geocode the customer’s address and check which polygon contains it. MapLibre handles this client-side with map.queryRenderedFeatures(), or you can do a server-side point-in-polygon check.
| Zone | Drive Time | Delivery Fee |
|---|---|---|
| Green | 0 — 10 min | Free |
| Yellow | 10 — 20 min | $5.00 |
| Red | 20 — 30 min | $10.00 |
| Outside red | 30+ min | No delivery |
Full source
Section titled “Full source”The complete app is at github.com/plazafyi/example-apps/tree/main/delivery-zones.
Built with Next.js + MapLibre GL JS.
Variations to try
Section titled “Variations to try”Walking zones for a neighborhood restaurant. Switch profile to foot and use shorter times: contours_minutes=5,10,15. Great for lunch spots.
Bike courier delivery. Use profile=bicycle with times like 10,15,20. Bike couriers cover more ground than walking but follow different routes than cars.
Dynamic pricing. Instead of fixed tiers, calculate the fee as a function of drive time: fee = Math.max(0, (driveMinutes - 10) * 0.50). Show the estimated fee in a popup when a customer clicks their location.
Click-to-check. Add a click handler that tells you which zone a point falls in:
map.on("click", (e) => { const features = map.queryRenderedFeatures(e.point, { layers: ["zone-0-fill", "zone-1-fill", "zone-2-fill"], }); if (features.length) { const zoneIndex = parseInt(features[0].layer.id.split("-")[1]); new maplibregl.Popup() .setLngLat(e.lngLat) .setHTML(`Delivery fee: <b>${ZONES[zoneIndex].fee}</b>`) .addTo(map); }});