Leaflet Isochrone Reachability Map
blocklogisticsDrive-time reachability zones showing 10m, 20m, and 30m commute polygons with workforce population metrics — on free OpenStreetMap/Esri tiles, no API key.
Installation
$pnpm dlx uipkge-ng@latest add leaflet-isochrone-reachability-map$npx uipkge-ng@latest add leaflet-isochrone-reachability-map$yarn dlx uipkge-ng@latest add leaflet-isochrone-reachability-map$bunx uipkge-ng@latest add leaflet-isochrone-reachability-mapManifest:
https://uipkge.dev/r/angular/leaflet-isochrone-reachability-map.jsonInstalls to:src/app/components/blocks/leaflet-isochrone-reachability-map/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
IsochroneBandinterface IsochroneBand {
coords: [number, number][]
pop: string
jobs: string
color: string
label: string
}IsochroneDestinationinterface IsochroneDestination {
name: string
lngLat: [number, number]
}Includes
Files installed (2)
src/app/components/blocks/leaflet-isochrone-reachability-map/leaflet-isochrone-reachability-map.component.ts8.8 kB
// Drive-time reachability zones showing 10m, 20m, and 30m commute polygons with workforce // population metrics — on free OpenStreetMap/Esri tiles, no API key. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiLeafletMapComponent, UiLeafletMarkerComponent, UiLeafletPolygonComponent, } from '@/ui/leaflet-map/leaflet-map.component' export type IsochroneMinutes = 10 | 20 | 30 export interface IsochroneBand { coords: [number, number][] pop: string jobs: string color: string label: string } export const HUB_LOCATION: [number, number] = [-122.419, 37.775] // Downtown San Francisco // Key-free drive-time approximation: a perturbed ring whose radius varies by // angle through fixed harmonic offsets, so each band reads as an irregular // blob rather than a perfect circle (a real isochrone needs a routing API). export function createIsochroneCoords(radiusMiles: number, phase: number): [number, number][] { const points: [number, number][] = [] const steps = 48 const latDelta = radiusMiles / 69 const lngDelta = radiusMiles / (69 * Math.cos((HUB_LOCATION[1] * Math.PI) / 180)) for (let i = 0; i <= steps; i++) { const angle = (i / steps) * 2 * Math.PI const jitter = 1 + 0.16 * Math.sin(angle * 3 + phase) + 0.07 * Math.sin(angle * 7 + phase * 2.3) const lng = HUB_LOCATION[0] + lngDelta * Math.cos(angle) * jitter const lat = HUB_LOCATION[1] + latDelta * Math.sin(angle) * jitter points.push([lng, lat]) } return points } export const ISOCHRONES: Record<IsochroneMinutes, IsochroneBand> = { 10: { coords: createIsochroneCoords(2.8, 0.4), pop: '340K', jobs: '520K', color: '#10b981', label: '10 Min Commute', }, 20: { coords: createIsochroneCoords(6.2, 1.7), pop: '1.24M', jobs: '890K', color: '#3b82f6', label: '20 Min Commute', }, 30: { coords: createIsochroneCoords(11.5, 2.9), pop: '2.85M', jobs: '1.45M', color: '#f59e0b', label: '30 Min Commute', }, } export interface IsochroneDestination { name: string lngLat: [number, number] } export const DESTINATIONS: IsochroneDestination[] = [ { name: 'Mission Depot', lngLat: [-122.414, 37.762] }, { name: 'Daly City Hub', lngLat: [-122.47, 37.705] }, { name: 'Oakland Annex', lngLat: [-122.272, 37.804] }, ] const TIME_OPTIONS: IsochroneMinutes[] = [10, 20, 30] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-leaflet-isochrone-reachability-map, [ui-leaflet-isochrone-reachability-map]', standalone: true, imports: [ UiBadgeComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiLeafletMapComponent, UiLeafletMarkerComponent, UiLeafletPolygonComponent, ], host: { 'data-slot': 'leaflet-isochrone-reachability-map', '[class]': 'hostClass', }, template: ` <div class="border-border bg-card relative h-[480px] overflow-hidden rounded-xl border"> <ui-leaflet-map variant="light" [center]="hubLocation" [zoom]="10.5" class="absolute inset-0 size-full"> <!-- 30m Isochrone (outermost, drawn first) --> <ui-leaflet-polygon [lngLatPath]="isochrones[30].coords" [color]="isochrones[30].color" [weight]="1.5" dashArray="4 4" [fill]="true" [fillColor]="isochrones[30].color" [fillOpacity]="selectedTime() >= 30 ? 0.15 : 0.04" /> <!-- 20m Isochrone --> <ui-leaflet-polygon [lngLatPath]="isochrones[20].coords" [color]="isochrones[20].color" [weight]="2" [fill]="true" [fillColor]="isochrones[20].color" [fillOpacity]="selectedTime() >= 20 ? 0.22 : 0.06" /> <!-- 10m Isochrone (innermost, most opaque) --> <ui-leaflet-polygon [lngLatPath]="isochrones[10].coords" [color]="isochrones[10].color" [weight]="2.5" [fill]="true" [fillColor]="isochrones[10].color" [fillOpacity]="0.3" /> <!-- Destinations --> @for (dest of destinations; track dest.name) { <ui-leaflet-marker [lngLat]="dest.lngLat" anchor="center"> <div class="flex flex-col items-center gap-0.5"> <span class="border-background bg-foreground size-2 rounded-full border-2 shadow-xs"></span> <span class="bg-card/90 text-foreground rounded px-1 font-mono text-xs font-semibold whitespace-nowrap shadow-xs" > {{ dest.name }} </span> </div> </ui-leaflet-marker> } <!-- Central Hub Marker --> <ui-leaflet-marker [lngLat]="hubLocation" anchor="center"> <div class="relative flex items-center justify-center"> <span class="bg-primary/30 absolute size-6 animate-ping rounded-full"></span> <div class="bg-primary text-primary-foreground ring-background flex size-6 items-center justify-center rounded-full font-mono text-xs font-bold shadow-md ring-2" > ★ </div> </div> </ui-leaflet-marker> </ui-leaflet-map> </div> <!-- Isochrone Analytics Card --> <ui-card class="flex flex-col justify-between"> <ui-card-header> <div class="flex items-center justify-between"> <span ui-badge variant="outline" class="font-mono text-xs">COMMUTE ANALYSIS</span> <span ui-badge class="border-info/20 bg-info/10 text-info">Drive-Time</span> </div> <ui-card-title class="mt-2 text-lg">Reachability Zones</ui-card-title> <ui-card-description>Transit reachability from Downtown Distribution Hub.</ui-card-description> </ui-card-header> <ui-card-content class="space-y-4"> <!-- Range Selector Tabs --> <div class="border-border bg-muted grid grid-cols-3 gap-1 rounded-lg border p-1 text-xs"> @for (t of timeOptions; track t) { <button type="button" [class]="timeTabClass(t)" (click)="selectedTime.set(t)">{{ t }} mins</button> } </div> <div class="border-border space-y-3 border-t pt-3"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs">Population Reach</span> <span class="text-foreground font-mono text-base font-bold">{{ activeBand().pop }}</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs">Workforce / Labor Pool</span> <span class="text-foreground font-mono text-base font-bold">{{ activeBand().jobs }}</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs">Service Band</span> <span class="font-mono text-xs font-semibold" [style.color]="activeBand().color"> {{ activeBand().label }} </span> </div> </div> <div class="border-border text-muted-foreground space-y-2 border-t pt-3 text-xs"> <div class="flex items-center gap-2"> <span class="bg-success size-2 rounded-full"></span> <span>Green ring: 10-minute urban core</span> </div> <div class="flex items-center gap-2"> <span class="bg-info size-2 rounded-full"></span> <span>Blue ring: 20-minute metropolitan belt</span> </div> <div class="flex items-center gap-2"> <span class="bg-warning size-2 rounded-full"></span> <span>Amber ring: 30-minute outer ring</span> </div> </div> </ui-card-content> </ui-card> `, }) export class UiLeafletIsochroneReachabilityMapComponent { @Input('class') className?: string protected readonly hubLocation = HUB_LOCATION protected readonly isochrones = ISOCHRONES protected readonly destinations = DESTINATIONS protected readonly timeOptions = TIME_OPTIONS readonly selectedTime = signal<IsochroneMinutes>(20) get hostClass(): string { return cn('grid gap-4 lg:grid-cols-[1fr_320px]', this.className) } activeBand(): IsochroneBand { return this.isochrones[this.selectedTime()] } timeTabClass(t: IsochroneMinutes): string { return cn( 'rounded-md py-1.5 font-medium transition-colors', this.selectedTime() === t ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } }src/app/components/blocks/leaflet-isochrone-reachability-map/index.ts0.3 kB
export { UiLeafletIsochroneReachabilityMapComponent, HUB_LOCATION, ISOCHRONES, DESTINATIONS, createIsochroneCoords, type IsochroneBand, type IsochroneDestination, type IsochroneMinutes, } from './leaflet-isochrone-reachability-map.component'
Raw manifest:https://uipkge.dev/r/angular/leaflet-isochrone-reachability-map.json