Isochrone Reachability Map
blocklogisticsDrive-time reachability zones showing 10m, 20m, and 30m commute polygons with workforce population metrics.
Installation
$pnpm dlx uipkge-ng@latest add isochrone-reachability-map$npx uipkge-ng@latest add isochrone-reachability-map$yarn dlx uipkge-ng@latest add isochrone-reachability-map$bunx uipkge-ng@latest add isochrone-reachability-mapManifest:
https://uipkge.dev/r/angular/isochrone-reachability-map.jsonInstalls to:src/app/components/blocks/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.
IsochroneZoneinterface IsochroneZone {
coords: [number, number][]
pop: string
jobs: string
color: string
label: string
}Files installed (2)
src/app/components/blocks/isochrone-reachability-map/isochrone-reachability-map.component.ts9.2 kB
import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import type * as mapboxgl from 'mapbox-gl' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiMapComponent, UiMapMarkerComponent, type MapLayerInput, type MapSourceInput } from '@/ui/map/map.component' export type IsochroneTime = 10 | 20 | 30 interface IsochroneZone { coords: [number, number][] pop: string jobs: string color: string label: string } const HUB_LOCATION: [number, number] = [-87.6298, 41.8781] // Chicago Loop const HUB_LAT = 41.8781 const TIME_OPTIONS: IsochroneTime[] = [10, 20, 30] // Approximated polygon coordinates for 10m, 20m, 30m isochrones function createIsochroneCoords(radiusMiles: number): [number, number][] { const points: [number, number][] = [] const steps = 36 const latDelta = radiusMiles / 69 const lngDelta = radiusMiles / (69 * Math.cos((HUB_LAT * Math.PI) / 180)) for (let i = 0; i <= steps; i++) { const angle = (i / steps) * 2 * Math.PI // Add small organic jitter const jitter = 1 + Math.sin(angle * 3) * 0.12 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 } function buildZones(): Record<IsochroneTime, IsochroneZone> { return { 10: { coords: createIsochroneCoords(2.8), pop: '340K', jobs: '520K', color: '#10b981', label: '10 Min Commute' }, 20: { coords: createIsochroneCoords(6.2), pop: '1.24M', jobs: '890K', color: '#3b82f6', label: '20 Min Commute' }, 30: { coords: createIsochroneCoords(11.5), pop: '2.85M', jobs: '1.45M', color: '#f59e0b', label: '30 Min Commute' }, } } const ISOCHRONES = buildZones() function polygonSource(id: string, coords: [number, number][]): MapSourceInput { return { id, type: 'geojson', data: { type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [coords] }, }, } } /** * IsochroneReachabilityMap -- the React block, 1:1: drive-time reachability zones showing * 10m, 20m, and 30m commute polygons on a Mapbox map with a workforce analytics card * (COMMUTE ANALYSIS, Drive-Time, Reachability Zones, Transit reachability from Downtown * Distribution Hub) and a 10/20/30 min range selector driving population, labor pool and * service band metrics. Sources are static GeoJSON; layer fill emphasis follows the * selected time, live-patched onto the map when it exists. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-isochrone-reachability-map, [ui-isochrone-reachability-map]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ UiBadgeComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiMapComponent, UiMapMarkerComponent, ], template: ` <div data-slot="isochrone-reachability-map" [class]="rootClass"> <div class="border-border bg-card relative h-[480px] overflow-hidden rounded-xl border"> <div ui-map [accessToken]="accessToken" variant="light" [center]="hubLocation" [zoom]="10.8" class="h-full w-full" [sources]="mapSources" [layers]="mapLayers()" (created)="onMapCreated($event)" > <!-- Central Hub Marker --> <ui-map-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-map-marker> </div> </div> <!-- Isochrone Analytics Card --> <div ui-card class="flex flex-col justify-between"> <div 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> <h3 ui-card-title class="mt-2 text-lg">Reachability Zones</h3> <p ui-card-description>Transit reachability from Downtown Distribution Hub.</p> </div> <div 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="rounded-md py-1.5 font-medium transition-colors" [class]=" selectedTime() === t ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground' " (click)="selectTime(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">{{ activeZone().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">{{ activeZone().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]="activeZone().color"> {{ activeZone().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> </div> </div> </div> `, }) export class UiIsochroneReachabilityMapComponent { protected readonly timeOptions = TIME_OPTIONS protected readonly hubLocation = HUB_LOCATION @Input() accessToken = '' @Input('class') className?: string readonly selectedTime = signal<IsochroneTime>(20) readonly activeZone = computed<IsochroneZone>(() => ISOCHRONES[this.selectedTime()]) /** Static GeoJSON polygons: 30m outer ring first so the 10m core paints on top. */ readonly mapSources: MapSourceInput[] = (TIME_OPTIONS.slice().reverse() as IsochroneTime[]).map((t) => polygonSource(`iso-${t}`, ISOCHRONES[t].coords), ) /** Fill emphasis follows the selected time; lines stay constant. */ readonly mapLayers = computed<MapLayerInput[]>(() => { const selected = this.selectedTime() const layers: MapLayerInput[] = [] for (const t of TIME_OPTIONS.slice().reverse() as IsochroneTime[]) { const zone = ISOCHRONES[t] const fillOpacity = t === 10 ? 0.3 : t === 20 ? (selected >= 20 ? 0.22 : 0.06) : selected >= 30 ? 0.15 : 0.04 layers.push({ id: `iso-${t}-fill`, type: 'fill', source: `iso-${t}`, paint: { 'fill-color': zone.color, 'fill-opacity': fillOpacity }, }) layers.push({ id: `iso-${t}-line`, type: 'line', source: `iso-${t}`, paint: t === 30 ? { 'line-color': zone.color, 'line-width': 1.5, 'line-dasharray': [2, 2] } : { 'line-color': zone.color, 'line-width': t === 10 ? 2.5 : 2 }, }) } return layers }) private map: mapboxgl.Map | null = null get rootClass(): string { return cn('grid gap-4 lg:grid-cols-[1fr_320px]', this.className) } selectTime(t: IsochroneTime): void { this.selectedTime.set(t) // The map only adds missing layers on input change, so patch live paint directly. try { this.map?.setPaintProperty('iso-30-fill', 'fill-opacity', t >= 30 ? 0.15 : 0.04) this.map?.setPaintProperty('iso-20-fill', 'fill-opacity', t >= 20 ? 0.22 : 0.06) } catch { /* map not ready yet; the computed layers cover first paint */ } } onMapCreated(map: mapboxgl.Map): void { this.map = map } }src/app/components/blocks/isochrone-reachability-map/index.ts0.1 kB
export { UiIsochroneReachabilityMapComponent, type IsochroneTime } from './isochrone-reachability-map.component'
Raw manifest:https://uipkge.dev/r/angular/isochrone-reachability-map.json