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-properlabels

v1.0.0

Published

tiny maplibre plugin to place labels properly for tiled geometries

Readme

maplibre-proper-labels

npm version jsDelivr

Maplibre GL JS plugin for proper labelling of polygons that extend across tiles.

img

(red labels are the "proper" ones 😅)

Live example at

https://abelvm.github.io/maplibre-properlabels/example/

Why

Any tiled-sourced vector layer in MapLibre lacks proper labelling, as every geometry that extends through several tiles has several labels, one per geometry portion.

This just grin my gears

img

This is inspired by https://github.com/maplibre/maplibre-tile-spec/issues/710 and my stubbornness

How to use

Build

Just grab the files in the dist folder, or run npm run build to regenerate those files

Install

Install from npm (recommended):

npm install maplibre-properlabels

Then import in your project:

import ProperLabels from 'maplibre-properlabels';
// or, if using CommonJS:
// const ProperLabels = require('maplibre-properlabels').default;

Use via CDN (jsDelivr / unpkg):

<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/maplibre-properlabels.js"></script>
<!-- or unpkg -->
<script src="https://unpkg.com/[email protected]/dist/maplibre-properlabels.js"></script>

And then

const proper = new ProperLabels({
        map: map,
        source: 'demotiles', 
        sourceLayer: 'countries'
    });

When using the CDN bundle the plugin registers itself on maplibregl.VectorTileSource.prototype and can also be used like this:

const mysource = map.getSource('demotiles');
const proper = mysource.ProperLabels({
        // no need to provide `map` or `source` as they are implicit in `mysource`
        sourceLayer: 'countries'
    });

Use

Initialize the plugin once the map is ready. The constructor accepts an options object:

| name | type | description | optional | default | |---|---|---|---|---| | map | Maplibre Map instance | The map instance | required | — | | source | string | Vector tile source id, or a VectorTileSource object | required | — | | sourceLayer | string | The inner layer name inside the vector tiles to label | required | — | | fid | string | Property name to promote as feature id (promoteId) | optional | id | | tolerance | number | Simplify / polylabel precision (degrees) | optional | 0.00001 | | cacheSize | number | Worker-side cache capacity (entries) | optional | 10000 | | postDelay | number | Debounce delay (ms) before posting features to worker | optional | 100 |

Example (see example/index.html):

map.on('load', () => {
    const proper = new ProperLabels({
        map,
        source: 'demotiles',       // can also be a VectorTileSource object
        sourceLayer: 'countries',
        fid: 'fid',                // optional, property used as promoted id
        tolerance: 0.00001,
        cacheSize: 10000
    });

    // The plugin creates a GeoJSON source named `${sourceId}-proper`.
    // Use it when adding a label layer:
    map.addLayer({
        id: 'countries-labels-proper',
        type: 'symbol',
        source: 'demotiles-proper',
        layout: {
            'text-field': ['coalesce', ['get', 'name'], ['get', 'name_en'], ['get', 'NAME'], ''],
            'text-size': 12
        },
        paint: { 'text-color': '#ff0000' }
    });
});

How does it work

I've spent several days trying to put a man in the middle of the lifecycle of the features bucket of MaplibreGL JS, to upstream this functionality, but regardless the approach... the rendered always picked the raw features instead of the processed ones, so, long story short, this is a plugin instead of a PR. And, as a plugin without access to internals, it's not as elegant as it could be. Meh.

And how does it work?

  1. On new data loading the plugin queries the vector-tile source for all loaded features using map.querySourceFeatures(sourceId, { sourceLayer }).
  2. Features are grouped by the promoted id so every logical feature (which may be split across tiles) is processed as a single group.
  3. The main thread encodes the groups into a compact binary transferable (Float32 coordinate buffer + key-indexed properties buffer) and posts it to a worker. An ArrayBufferPool is used to reduce allocations.
  4. The worker decodes the binary payload, runs geometry processing (simplify, union/flatten/combine for multi-part groups, and a safe polylabel fallback), and computes a short raw-group signature and geometry hashes to detect unchanged items.
  5. The worker keeps a cache of processed features and emits incremental diffs (adds/updates/removes). Add/update feature lists are encoded as binary transferables and property diffs are compacted into a shared keys table + props buffer to minimize structured-clone cost.
  6. The main thread decodes the binary diffs, reconstructs a canonical GeoJSONSourceDiff and applies it with source.updateData(diff). A short handshake (diff_ack) lets the worker commit pending changes to its cache only after the main thread successfully applied the diff.

This design keeps the main thread lightweight by transferring buffers, applying incremental diffs, and avoiding expensive geometry work on the UI thread.

Local development

To run the example locally:

  1. Install dependencies
npm install
  1. Start the dev server (Vite serves the example at /example)
npm run dev
# open http://localhost:5173/example/

Or build the package and open example/index.html after npm run build.

Performance & implementation notes

  • The plugin offloads heavy geometry processing to a worker and uses compact binary transferables (Float32 coords + key-indexed properties) to minimize main-thread cost.
  • Diffs between runs are encoded as binary transfer messages so updateData can be applied with minimal structured-clone overhead.
  • Geometry hashing uses a lightweight Float32-based hash with a small deep-equality fallback to avoid unnecessary recomputation.
  • For debugging, enable tile boundaries with map.showTileBoundaries = true and use the example legend to correlate labels and clipped geometry.