
Introduction
At State Farm, we always try to be a good neighbor and today we are excited to announce we are giving back to the open source community with a new NPM package for labeling polygons called polygon-labeler. In this post, we’ll explore its features, demonstrate how to use it, and highlight what sets it apart from other polygon labeling solutions.
Problem
Placing labels on polygons sounds simple: find the center of a polygon and render a label there. In practice, this logic produces incorrect label placements or labels that are invisible to the user. At State Farm, we operate our own internal mapping PaaS and required a solution that was both accurate and high performing for labeling polygons within our platform. While we explored existing packages like polylabel and Turf, each presented limitations that inspired me to develop polygon-labeler. This lightweight, specialized package consistently computes optimal label points for both individual and grouped polygon features. In this post, I’ll cover common labeling challenges, explain how polygon-labeler addresses them, and show you how to integrate it into your mapping application.
polygon-labeler
A package for generating a single ideal location to label polygons. This is useful for mapping libraries like Maplibre GL that will generate multiple labels for polygons by default. The package uses an algorithm to find the visual center of polygons and supports grouping features by properties, handling multi-polygons, and viewport clipping for optimal performance.
Centroids

When trying to label a set of polygons the first solution that people may gravitate towards is using the centroid of each polygon. While on paper this may sound great there are many pitfalls to using centroids. Turf.js provides a method called centroid that allows you to easily compute the centroid by passing in each feature.
Why centroids are a poor default:
- Centroids are calculated via the mean of all vertices within the polygon. For concave shapes, centroids often lie outside the polygon entirely.
- For MultiPolygons and features with disjoint islands, like Hawaii, a single centroid can land in the water between the islands or inside a tiny island instead of the largest visible landmass.
- Map panning and tiling makes the problem worse while choosing centroids. If you pre-compute centroids on geometries but display a clipped view, the centroid may fall outside the clipped area or even worse not at all.
Point on Feature

Using a point that lies on the polygon boundary,for example pointOnFeature from Turf, is a common alternative to centroids because it guarantees the returned location is on the geometry. However, this approach has its own pitfalls when used for labeling.
Why point on features are a poor default:
- The point may end up on the polygon’s outer edge or on a narrow sliver.
- For complex polygons, pointOnFeature often returns a point on a small polygon rather than the visually dominant area.
- Points on edges can cause label collision detection to behave poorly, this leads to polygons not being labeled
These conditions lead to labels that overlap unrelated features, sit off the polygon, or are invisible on the current map viewport. For readable maps and consistent UX, we need a label position that is visually central and lies on or inside the polygon that is actually visible to the user.
Why should I use this package over polylabel?
polylabel is a great package, we even use it inside polygon-labeler, to find the pole of inaccessibility (a visually centered interior point) for a polygon, but it solves one of the many problems with creating dynamic labels. polylabel is effective at finding the polygon’s greatest interior point, but it doesn’t address several key challenges involved in labeling polygons within web mapping applications covered below.
Key Challenges
Single Polygon Geometry
For MultiPolygons it returns a point on every tiny island or disjoint part instead of the visually dominant polygon.

Viewport Awareness
polylabel does not clip polygons to the current map view. This may lead to the label being generated outside of the current viewport. In the example below, we can see the label was generated outside of the map viewport and would not be visible to the user.

Fallback Handling
In some cases, polylabel cannot guarantee a point within the polygon. This edge case leads to labels being placed potentially in neighboring polygons, in unintentional locations, or nowhere.
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"coordinates": [
[
[
-111.1815838,
45.7768091
],
[
-111.1816944,
45.776695
],
[
-111.1812484,
45.7764847
],
[
-111.1811378,
45.7765988
],
[
-111.1815838,
45.7768091
]
]
],
"type": "Polygon"
}
}
]
}
[NaN, NaN, distance: NaN]
Design goals for polygon-labeler
When building polygon-labeler
- Guarantee the point is inside the polygon.
- Always return a point that is visually central to the most relevant polygon geometry.
- Respect grouping and unique identifiers so multi-part features are labeled where users expect them.
- Support clipping to the current map view so labels remain visible and meaningful.
How polygon-labeler works
The package determines optimal label placement through the following steps:
- Group Features: Features are grouped by a unique identifier property (e.g., “state_name”). All polygons with the same identifier are collected together.
- Clip to Viewport: Only retain polygons that are clipped to fall within the current map view bounds. This ensures labels are visible on the map.
- Find Largest Polygon: For each group, the package calculates the area of all polygons and identifies the largest polygon by area. This ensures the label is placed on the most prominent part of the feature.
- Calculate Pole Of Inaccessibility: Using polylabel, determine the most distant internal point from the polygon outline for the largest polygon.
- Validate Position: The package checks if the point falls within the polygon boundaries: If inside: The pole of inaccessibility is used as the label point. If outside: If the point falls outside the polygon, calculate a point on the feature to be used.
- Return GeoJSON: The result is a GeoJSON FeatureCollection of Point features, each representing an optimal label location with the specified property attached.
Installation
npm install @statefarmins/polygon-labeler
Usage
import { getLabelPoints } from '@statefarmins/polygon-labeler';
import type { FeatureCollection, Polygon } from 'geojson';
const featureCollection: FeatureCollection<Polygon> = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [[[0, 0], [0, 4], [4, 4], [4, 0], [0, 0]]],
},
properties: { name: "Area1", id: "1" },
},
],
};
// Define map bounds
const southWest = { lat: -10, lng: -10 };
const northEast = { lat: 10, lng: 10 };
// Get label points
const labelPoints = getLabelPoints(
featureCollection,
'name',
'id',
southWest,
northEast
);
console.log(labelPoints);
Output
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [ 2, 2 ]
},
"properties": { "name": "Area1" }
}
]
}
Example Image

Maplibre GL Usage
import maplibregl from 'maplibre-gl';
import { getLabelPoints } from '@statefarmins/polygon-labeler';
let lastBounds: { sw: { lat: number; lng: number }; ne: { lat: number; lng: number } } | null = null;
let lastZoom: number | null = null;
/**
* Determine if labels should be recalculated
*
* @name shouldRecalculateLabels
* @param {map} maplibregl.Map A maplibre map
* @param {number} tolerance The amount of allowed shift in map bounds in degrees
* @returns {boolean} if labels should be recalculated
*/
function shouldRecalculateLabels(map: maplibregl.Map, tolerance = 0.00001): boolean {
const bounds = map.getBounds();
const zoom = map.getZoom();
const sw = { lat: bounds.getSouth(), lng: bounds.getWest() };
const ne = { lat: bounds.getNorth(), lng: bounds.getEast() };
if (!this.lastBounds || !this.lastZoom) return true;
if (Math.abs(this.lastZoom - zoom) > 0.01) return true;
return (
Math.abs(this.lastBounds.sw.lat - sw.lat) > tolerance ||
Math.abs(this.lastBounds.sw.lng - sw.lng) > tolerance ||
Math.abs(this.lastBounds.ne.lat - ne.lat) > tolerance ||
Math.abs(this.lastBounds.ne.lng - ne.lng) > tolerance
);
}
/**
* Generate unique labels for each feature
*
* @name generatePolygonLabels
* @param {map} maplibregl.Map A maplibre map
* @param {str} layerName The name of the source for the layer
* @param {str} labelField The field used to label each polygon
* @param {str} uniqueIdentifierField The field used to distinguish unique features
* @returns {boolean} if labels should be recalculated
*/
function generatePolygonLabels(map: maplibregl.Map, layerName: str, labelField: str, uniqueIdentifierField: str) {
if (!this.shouldRecalculateLabels(map)) {
return;
}
const bounds = map.getBounds();
const sw = { lat: bounds.getSouth(), lng: bounds.getWest() };
const ne = { lat: bounds.getNorth(), lng: bounds.getEast() };
this.lastBounds = { sw, ne };
this.lastZoom = map.getZoom();
polygonFeatures = map.queryRenderedFeatures({
layers: [layerName]
});
let polygonFeatureCollection = {
type: "FeatureCollection",
features: polygonFeatures
}
const polygonLabelPoints = getLabelPoints(
polygonFeatureCollection,
labelField,
uniqueIdentifierField,
sw,
ne
);
map.getSource(`${layerName}_geojson`).setData(polygonLabelPoints);
}
/**
* Reset the zoom and bounds
*
* @name clearCache
*/
function clearCache() {
lastBounds = null;
lastZoom = null;
}
map.on('idle', () => {
generatePolygonLabels(map, "polygons", "name", "id");
});
Common pitfalls and recommendations
Even with an optimized labeling solution, there are a few best practices to keep in mind when integrating polygon-labeler into your mapping application
Avoid static label computation
Computing labels only once for an entire dataset and reusing them across all map views is a common mistake. As users pan and zoom, the visible portion of a polygon changes dramatically. A label point calculated for the full geometry may fall outside the current viewport, leaving users with unlabeled features. Instead, either clip geometries to the viewport before computing labels or recalculate labels dynamically as the view changes. The Maplibre GL example above demonstrates this pattern using the shouldRecalculateLabels function to trigger updates only when the map bounds shift significantly.
Handle small polygons gracefully
For very small polygons, such as tiny islands or narrow slivers, the visual center may be virtually indistinguishable from the centroid. In these edge cases, the sophisticated pole-of-inaccessibility calculation provides little benefit over simpler methods. When dealing with such features, prioritize label visibility and rely on your mapping library’s built-in collision detection to ensure labels remain readable. Consider adjusting label priority or styling to prevent small polygon labels from cluttering the map at certain zoom levels.
Cache and throttle updates
Recalculating labels on every frame or minor map movement can degrade performance, especially with large datasets. Implement caching strategies and tolerance thresholds to avoid unnecessary recomputation. The example code demonstrates this with lastBounds and lastZoom tracking, only recalculating when the view has shifted beyond a defined tolerance.
npm Link
To learn more about technology careers at State Farm, or to join our team visit, https://www.statefarm.com/careers.
Information contained in this article may not be representative of actual use cases. The views expressed in the article are personal views of the author and are not necessarily those of State Farm Mutual Automobile Insurance Company, its subsidiaries and affiliates (collectively “State Farm”). Nothing in the article should be construed as an endorsement by State Farm of any non-State Farm product or service.
<hr /><p>Responsive Labeling with polygon-labeler — State Farm Open Source was originally published in State Farm Engineering Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>