Skip to content
Go back

SVG Polygon Patterns for MapLibre

Published:  at  08:00 AM

The problem

MapLibre renders vector tile polygons with solid colour fills. Solid colour works for most use cases, but patterns unlock three things colour alone cannot do.

First, accessibility. Around 8% of men have colour vision deficiency. Distinctions that are obvious to most people — red vs green, blue vs purple — blur together. Patterns work for everyone, regardless of how they perceive colour.

Second, print. Maps printed in grayscale lose every colour-based distinction. Patterns survive the transition to black and white.

Third, two-dimensional encoding. Solid fill encodes one variable: colour. Patterns add a second visual channel. You can use colour for one metric and pattern for another, which opens up readability in ways colour alone cannot.

The problem is that MapLibre’s WebGL renderer does not accept SVG directly. Every pattern must go through a conversion pipeline — SVG to Canvas to ImageData to WebGL texture — and be registered with map.addImage(). This plugin wraps that pipeline, adds caching, and provides expression builders so patterns work with any GeoJSON source.

World map with 177 countries each assigned a unique SVG hatch pattern, zero adjacency conflicts

How it works

The plugin uses textures.js — a tiny SVG pattern library — to generate 28 built-in patterns: lines, circles, hexagons, waves, paths, and more. Each pattern is rendered to a hidden canvas at 4x resolution (to stay sharp on Retina displays), converted to ImageData, and registered with the map.

const patterns = new PolygonPatterns(map, { prewarmOnLoad: true });
await patterns.ensure("lines-crossed", "#c65b2e");

The ensure(patternId, colour) method is the core. It checks a cache first — if that pattern+colour combination was already generated, it returns instantly. Otherwise it runs the full SVG→Canvas→WebGL pipeline and registers the image.

Patterns are referenced in a layer through fill-pattern. The plugin provides two expression builders:

This means every polygon in a single layer can have a different pattern and colour. The expression routes each feature to its registered image based on its properties.

Colorize

Adjacent polygons should not share the same pattern. The colorize() static method assigns patterns to polygons so that neighbours always get different ones.

const assignment = PolygonPatterns.colorize(
  features.map((f) => ({ id: f.id, coordinates: f.geometry.coordinates })),
  PATTERN_IDS,
);
// Returns: [{ id: "PT", patternId: "lines-crossed" }, ...]

The method was ported from geoglify.com. I wrote about the algorithm — R-tree spatial filtering, edge hashing for exact adjacency, and randomised greedy graph coloring — in detail in the previous article Map Patterning: Assigning SVG Textures to Hundreds of Adjacent Polygons. The short version: it finds neighbours in ~5ms for 242 countries and assigns 28 patterns with zero conflicts.

How to use

Install with npm:

npm install @leoneljdias/maplibre-gl-polygon-patterns

Or from a script tag (UMD):

<script src="https://unpkg.com/@leoneljdias/maplibre-gl-polygon-patterns/dist/index.umd.js"></script>
<script>
  const patterns = new maplibregl.PolygonPatterns(map, options);
</script>

The “random mode” flow — assign a random pattern+colour to every polygon in a dataset — follows five steps:

  1. Pick colours and pattern IDs
  2. (Optional) Call PolygonPatterns.colorize() so adjacent polygons get different patterns
  3. Set geoglify:patternEnabled, geoglify:patternId, geoglify:patternColor, and geoglify:patternOpacity on each feature
  4. Call patterns.ensure() for every unique pattern+colour combination
  5. Use buildFillPatternExpr() and buildFillOpacityExpr() in your layer paint properties

The demo shows this end to end with world countries and a Regenerate button.

Bundle size

The plugin is about 51KB minified (textures.js and d3-selection are bundled in the dist). The colorize() algorithm adds no extra dependencies — it uses plain JavaScript with a built-in R-tree implementation.



Next Post
Label Callouts for MapLibre