PermitMap component provides an alternative MapLibre GL-based map view for NYC Permit Pulse. Unlike the isometric view in the App component, this displays permits on a traditional 2D slippy map with CartoDB dark base tiles.
Component Structure
ThePermitMap component is defined in src/PermitMap.tsx and serves as a standalone alternative to the OpenSeadragon isometric view.
State Management
Map State
Reference to the DOM element that hosts the MapLibre GL map
Reference to the MapLibre GL map instance
Reference to the active popup (detail tooltip)
Reference to the permit list container (for smooth scrolling)
Filter State
Start date for permit filtering (ISO date string, default: 7 days ago)
End date for permit filtering (ISO date string, default: today)
Set of selected job types (e.g., “NB”, “DM”, “GC”)
Set of selected boroughs (default: all 5 boroughs)
Search query string for filtering permits by address, description, or owner
Current sort field:
'date', 'cost', or 'address'Sort direction:
'asc' or 'desc'Data State
Complete array of permits loaded from the API (up to 2,500)
Array of permits after applying all client-side filters (type, borough, search)
True when permit data is being fetched
Error message if permit data fetch fails
Total count of permits in the date range (may exceed 2,500)
Currently selected permit (for highlighting and detail panel)
Whether the sidebar is open on mobile devices
Map Configuration
MAP_STYLE
src/PermitMap.tsx:20-40.
Map Initialization
src/PermitMap.tsx:73-119.
Key Functions
loadPermits
- Sets
loadingto true - Calls
fetchPermits(dateFrom, dateTo, RESULT_LIMIT)whereRESULT_LIMIT = 2500 - Updates
allPermitsstate on success - Sets
errorstate on failure - Sets
loadingto false
The API is capped at 2,500 results. If the date range contains more permits, the app shows a warning banner.
src/PermitMap.tsx:126-138.
updateMapDots
- Filters permits to those with valid
latitudeandlongitude - Converts to GeoJSON Feature collection with Point geometries
- Attaches properties:
color: Job type color fromgetJobColor()type: Job type codeaddress: Formatted addressdate: Issued date_raw: JSON-stringified permit object (for click handler)
- Updates the
permitsGeoJSON source
src/PermitMap.tsx:185-205.
selectPermit
- Updates
selectedPermitstate - Closes sidebar on mobile
- Flies to permit location with
map.flyTo()(zoom to level 15+) - Creates a MapLibre Popup with permit details HTML
- Scrolls the permit list to bring the selected row into view
src/PermitMap.tsx:207-226.
toggleType
selectedTypes Set.
See implementation at src/PermitMap.tsx:228-234.
toggleBorough
selectedBoroughs Set.
See implementation at src/PermitMap.tsx:236-242.
buildPopupHTML
- Job type label (color-coded with glow effect)
- Formatted address (bold)
- Issue date (gray, uppercase)
- Job description (truncated to 120 chars)
- Estimated cost (if available)
src/PermitMap.tsx:463-474.
Effect Lifecycle
Map Initialization Effect
When: Component mounts (once) Where:src/PermitMap.tsx:73-119
Steps:
- Creates MapLibre GL map instance
- Adds navigation control (zoom buttons)
- Registers ‘load’ handler that:
- Adds GeoJSON source:
permits - Adds circle layer:
permit-dotswith dynamic radius based on zoom - Registers click handler on
permit-dotsthat callsselectPermit() - Changes cursor to pointer on marker hover
- Adds GeoJSON source:
- Cleanup function removes map on unmount
Data Fetching Effect
When:dateFrom or dateTo changes
Where: src/PermitMap.tsx:122-124
Steps:
- Calls
loadPermits()to fetch new data
Client-Side Filtering Effect
When:allPermits, selectedTypes, selectedBoroughs, search, sortField, or sortDir changes
Where: src/PermitMap.tsx:141-183
Steps:
- Type filter: If not all types selected, filter by
job_type - Borough filter: If not all boroughs selected, filter by
borough - Search filter: If search query exists, filter by:
- Address (case-insensitive)
- Job description (case-insensitive)
- Owner business name (case-insensitive)
- Applicant business name (case-insensitive)
- Sort: Sort by selected field and direction:
date: Compareissued_datestringscost: Parseestimated_job_costsas float and compare numericallyaddress: Compare formatted addresses lexicographically
- Updates
filteredstate - Calls
updateMapDots(result)to refresh map layer
Sub-Components
DetailRow
Field label (e.g., “Borough”, “BIN”, “Status”)
Field value. If undefined or empty, returns
null (row not rendered)If
true, renders value in monospace font (used for filing numbers)src/PermitMap.tsx:453-461.
Permit Layer Styling
The permit markers are rendered as a MapLibre GL circle layer:- Zoom 10: 3px
- Zoom 14: 6px
- Zoom 17: 10px
src/PermitMap.tsx:93-104.
UI Components
Sidebar
The sidebar contains:- Header: Title + view switcher link to isometric view
- Date range picker: From/to inputs + quick select buttons (1d, 7d, 30d, 90d)
- Type filter: Chip grid with color-coded job type buttons
- Borough filter: Chip row with abbreviated borough names (MN, BK, QN, BX, SI)
- Search input: Full-text search across address, description, and owner
- Result count bar: Shows filtered count + warning if capped at 2,500
- Sort bar: Buttons to sort by date, cost, or address (with asc/desc indicator)
- Permit list: Scrollable list showing first 500 results (with overflow message)
Detail Panel
The detail panel (right side on desktop, overlay on mobile) shows:- Job type header (color-coded)
- Close button
- Address and issue date
- Borough
- Block/Lot
- BIN
- Status
- Description
- Estimated cost
- Owner
- Applicant
- Expiration date
- Filing number (monospace)
Map Legend
Fixed overlay in bottom-left corner showing:- NB (New Building)
- DM (Demolition)
- GC (General Construction)
- PL (Plumbing)
- ME (Mechanical)
- Other (gray)
Usage Example
The PermitMap component requires the following CSS files to be imported:
maplibre-gl/dist/maplibre-gl.css- MapLibre GL core styles./PermitMap.css- Component-specific styles
Performance Considerations
- Result limit: Caps API results at 2,500 permits to avoid performance issues
- Client-side filtering: All filters (type, borough, search, sort) run in the browser after data fetch
- List truncation: Only renders first 500 permits in the sidebar list (with overflow message)
- GeoJSON source: Uses MapLibre’s native GeoJSON rendering for efficient marker display
- Dynamic circle radius: Uses MapLibre expressions to scale markers with zoom level
- Memoized callbacks: Uses
useCallbackforloadPermits,updateMapDots, andselectPermit
Differences from Isometric View
Compared to theApp component:
| Feature | PermitMap (MapLibre) | App (OpenSeadragon) |
|---|---|---|
| Base map | CartoDB dark tiles (2D) | Isometric pixel-art NYC |
| Max permits | 2,500 (API limit) | Unlimited (chunked rendering) |
| Date picker | Custom from/to inputs | Fixed presets (7d/30d) |
| Search | Yes (address, description, owner) | No |
| Sort | Yes (date, cost, address) | No (always date desc) |
| Virtual scrolling | No (manual truncation at 500) | Yes (react-window) |
| Helicopters | No | Yes (live layer) |
| Neighborhood labels | No (CartoDB labels) | Yes (custom LOD labels) |
| Recency fade | No | Yes (opacity gradient) |
| Detail view | Side panel + popup | Drawer + tooltip |
Dependencies
react- Core React librarymaplibre-gl- Open-source map rendering library- Custom utilities:
fetchPermits,getJobColor,getJobLabel,formatAddress,formatDate - Permit type data:
ALL_JOB_TYPES,ALL_BOROUGHS,WORK_TYPE_LABELS,WORK_TYPE_COLORS
See Also
- App Component - OpenSeadragon isometric map view
- Permit Data - Data fetching and formatting functions