Skip to content
GuidesBlogPlaygroundDashboard

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.

  • 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

One API call returns all three contours. Pass comma-separated minute values and get back a GeoJSON FeatureCollection with one polygon per contour:

lib/plaza.js
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",
});
}

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" },
];

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 },
});
});

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.

ZoneDrive TimeDelivery Fee
Green0 — 10 minFree
Yellow10 — 20 min$5.00
Red20 — 30 min$10.00
Outside red30+ minNo delivery

The complete app is at github.com/plazafyi/example-apps/tree/main/delivery-zones.

Built with Next.js + MapLibre GL JS.

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);
}
});