npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

maplibre-zone-manager

v0.1.2

Published

Zone and polygon management for MapLibre GL + Terra Draw. Draw, edit and assign geographic zones to entities with marker support and state persistence.

Readme

maplibre-zone-manager

Zone and polygon management for MapLibre GL + Terra Draw. Draw, edit and assign geographic zones to entities — with markers, state persistence and keyboard shortcuts.

Use cases: delivery zones, sales territories, service areas, geofencing — anywhere you need to assign polygons to people or things on a map.

Features

  • Draw and edit polygons via Terra Draw
  • Assign zones to generic entities (sellers, drivers, regions…)
  • Configurable markers with category-based colors
  • State persistence in localStorage
  • Keyboard shortcuts (Delete, Escape)
  • Modal for marker details via injectable callback
  • Framework-agnostic core
  • Optional Stimulus adapter for Rails / Hotwire apps
  • Optional Rails nested-attributes adapter for saving
  • Optional Flatpickr date-range plugin

Install

# npm / yarn / pnpm
npm install maplibre-zone-manager maplibre-gl terra-draw terra-draw-maplibre-gl-adapter

# Rails import maps
bin/importmap pin maplibre-zone-manager

maplibre-gl, terra-draw and terra-draw-maplibre-gl-adapter are peer dependencies — you provide them.

Quick start

<link rel="stylesheet" href="https://unpkg.com/maplibre-gl/dist/maplibre-gl.css">
<div id="map" style="width: 100%; height: 500px"></div>
import { ZoneMap } from 'maplibre-zone-manager'

const map = new ZoneMap({
  container: document.getElementById('map'),
  center: { lat: -34.6, lng: -58.4 },
  zoom: 12,

  entities: [
    {
      id: 1,
      name: 'Alice',
      zones: [
        { id: 10, name: 'North', geometry: { type: 'Polygon', coordinates: [[...]] } }
      ]
    },
    { id: 2, name: 'Bob', zones: [] }
  ],

  markers: [
    { id: 1, lat: -34.61, lng: -58.38, title: 'Store A', category: 'recent' }
  ],

  markerColors: {
    recent: '#34A853',
    moderate: '#FBBC04',
    old: '#EA4335',
    default: '#9CA3AF'
  },

  onZonesSave: async (changes) => {
    const res = await fetch('/api/zones/bulk', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(changes)
    })
    return res.json() // { success: true, newZones: [...] }
  }
})

await map.ready()

// Select an entity and enter edit mode
map.selectEntity(1)
map.startEditing()
map.setMode('polygon') // start drawing

API

new ZoneMap(options)

| Option | Type | Default | Description | |--------|------|---------|-------------| | container | HTMLElement | required | Map container | | center | { lat, lng } | { lat: 0, lng: 0 } | Initial center | | zoom | number | 10 | Initial zoom | | style | object | string | OSM tiles | MapLibre style | | entities | array | [] | Entities with zones | | zonesKey | string | "zones" | Property name for zones in entity objects | | markers | array | [] | Marker data | | markerColors | object | {} | category → hex color map | | markerPopupTemplate | function | default | (data, id) => HTML | | drawStyles | object | — | { polygon: {...}, select: {...} } | | modal | HTMLElement | — | Modal container | | modalContent | HTMLElement | — | Modal content target | | fetchMarkerDetails | function | — | async (marker, filters) => HTML | | persistState | boolean | true | Enable localStorage persistence | | storageKey | string | "zone_map_state" | localStorage key | | keyboardShortcuts | boolean | true | Enable keyboard shortcuts |

Callbacks

| Callback | Signature | Description | |----------|-----------|-------------| | onReady | (zoneMap) => void | Map finished initializing | | onZonesSave | async (changes) => { success, newZones } | Save zones to your backend | | onMarkerClick | (markerData) => void | Marker clicked | | onEntitySelect | (entity) => void | Entity selected | | onModeChange | (mode) => void | Drawing mode changed | | onZoneCreated | (feature, entityId) => void | New polygon finished | | onZoneDeleted | (feature) => void | Polygon deleted |

Methods

map.selectEntity(id)     // Show zones for entity
map.showAllEntities()    // Show all zones from every entity
map.startEditing()       // Enter edit mode
map.stopEditing()        // Exit edit mode
map.setMode('polygon')   // Switch to drawing mode ('polygon' | 'select')
map.deleteSelectedZone() // Delete the selected polygon
map.toggleZones(bool)    // Show/hide zones
map.getChanges()         // { created, updated, deleted }
map.saveZones()          // Calls onZonesSave and applies server IDs
map.updateMarkers([...]) // Replace markers on the map
map.saveState()          // Persist to localStorage
map.restoreState()       // Restore from localStorage
map.destroy()            // Clean up

Change-set format

getChanges() and the onZonesSave callback receive:

{
  created: [
    { entityId: 1, name: 'Zone 2025-03-14', geometry: { type: 'Polygon', coordinates: [...] } }
  ],
  updated: [
    { id: 10, geometry: { type: 'Polygon', coordinates: [...] } }
  ],
  deleted: [11, 12]  // zone IDs
}

Your backend should return { success: true, newZones: [{ id, entityId }] } so that newly created zones get their server-assigned IDs mapped back.

Stimulus adapter (Rails / Hotwire)

For Hotwire apps, extend the base controller instead of wiring everything manually:

// app/javascript/controllers/zone_map_controller.js
import { Controller } from '@hotwired/stimulus'
import { createStimulusController } from 'maplibre-zone-manager/adapters/stimulus'

export default class extends createStimulusController(Controller) {
  async fetchMarkerDetails(marker, filters) {
    const res = await fetch(`/stores/${marker.id}/details`)
    return res.text()
  }

  async saveZones(changes) {
    const { RailsAdapter } = await import('maplibre-zone-manager/adapters/rails')

    const body = RailsAdapter.formatNestedAttributes(changes, {
      parentKey: 'company',
      attributesKey: 'seller_zones_attributes',
      entityForeignKey: 'seller_id'
    })

    const { headers, body: reqBody } = RailsAdapter.buildRequest(body)
    const res = await fetch('/company/seller_zones/bulk_update', {
      method: 'PATCH', headers, body: reqBody
    })
    return res.json()
  }
}
<div data-controller="zone-map"
     data-zone-map-lat-value="<%= @lat %>"
     data-zone-map-lng-value="<%= @lng %>"
     data-zone-map-zoom-value="12"
     data-zone-map-entities-value="<%= @sellers.to_json %>"
     data-zone-map-markers-value="<%= @stores.to_json %>">

  <div data-zone-map-target="map" style="height: 500px"></div>

  <button data-action="zone-map#startEditing">Edit zones</button>
  <button data-action="zone-map#save">Save</button>
</div>

Rails nested-attributes adapter

import { RailsAdapter } from 'maplibre-zone-manager/adapters/rails'

const changes = zoneMap.getChanges()

const payload = RailsAdapter.formatNestedAttributes(changes, {
  parentKey: 'company',
  attributesKey: 'seller_zones_attributes',
  entityForeignKey: 'seller_id',
  geometryKey: 'geometry_json'  // default
})

// payload =>
// {
//   company: {
//     seller_zones_attributes: {
//       "0": { seller_id: 1, name: "...", geometry_json: {...} },
//       "1": { id: 10, geometry_json: {...} },
//       "2": { id: 11, _destroy: true }
//     }
//   }
// }

Date-range plugin

Requires flatpickr as a peer dependency.

import { DateRangePlugin } from 'maplibre-zone-manager/plugins/date-range'

const dateRange = new DateRangePlugin({
  input: document.getElementById('date-range'),
  clearButton: document.getElementById('clear-date'),
  locale: 'es',
  onChange: (range) => {
    // range = { start: '2025-01-01', end: '2025-01-31' } | null
  }
})

dateRange.clear()
dateRange.destroy()

Using individual modules

Every module is independently importable:

import { PolygonManager } from 'maplibre-zone-manager'
import { MarkerBuilder } from 'maplibre-zone-manager'
import { MapStateManager } from 'maplibre-zone-manager'

Entity data format

Entities follow a simple convention:

{
  id: 1,
  name: 'Entity name',
  zones: [
    {
      id: 10,                       // server-assigned ID
      name: 'Zone name',
      geometry: {                    // GeoJSON Polygon
        type: 'Polygon',
        coordinates: [[[lng, lat], ...]]
      }
    }
  ]
}

The zones key is configurable via zonesKey. For Rails apps that use seller_zones, it also auto-detects that key.

Marker data format

{
  id: 1,
  lat: -34.61,
  lng: -58.38,
  title: 'Marker title',        // shown in popup
  description: 'Optional text',
  category: 'recent'            // maps to markerColors
}

Browser support

Same as MapLibre GL JS — all modern browsers.

License

MIT