Minimum Viable Clustered-Marker Globe using OpenFreeMap and MapLibre GL
I love OpenFreeMap it is a quick, easy, and free way to add beautiful maps to your Open Source projects. With the latest release of MapLibre-GL I wanted to see if there was an easy way to use both to make an interactive globe with clustered markers.
Spoiler alert: yes!
Basic Globe
Here's a basic example which I've trimmed down from this example.

When you load the below code, you'll get a globe which you can spin and zoom. Nifty!
HTML
<!DOCTYPE html> <html> <head> <title>Globe Projection using OpenFreeMap</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.0.0/dist/maplibre-gl.css"> <script src="https://unpkg.com/maplibre-gl@5.0.0/dist/maplibre-gl.js"></script> <style>style="color: #0000ff;"> body { style="color: #795E26;">margin: 0;style="color: #795E26;">padding: 0; }style="color: #0000ff;"> html, body, #map { style="color: #795E26;">height: 100%; } </style> </head> <body> <div id="map"></div> <script type="module"> const map = new maplibregl.Map({ container: "map", style: "https://tiles.openfreemap.org/styles/liberty", zoom: 2, center: [0.123, 51.2345], pitch: 0, canvasContextAttributes: { antialias: true } }); map.on('style.load', () => { map.setProjection({ type: 'globe', }); }); </script> </body> </html>
Adding Markers
In most respects, this acts like a normal MapLibre GL map. To add a marker, add this code:
JavaScript
const marker = new maplibregl.Marker() .setLngLat([12.345, 54.321]) .addTo(map);
GeoJSON Clusters
Now for the big one! On OpenBenches we display markers and clusters of markers. On a flat map, it looks like this:

We want to turn it into this beauty:
Here's the code, adapted from the tutorial:
JavaScript
const map = new maplibregl.Map({ container: "map", style: "https://tiles.openfreemap.org/styles/liberty", zoom: 2, center: [0.123, 51.2345], pitch: 0, canvasContextAttributes: { antialias: true } }); map.on('style.load', () => { map.setProjection({ type: 'globe', }); }); // Load GeoJSON async function load_GeoJSON() { const response = await fetch( "geo.json" ) var benches_json = await response.json(); return benches_json; } // Asynchronous function to add custom layers and sources async function addCustomLayersAndSources() { // Get the data var geo_data = await load_GeoJSON(); // Load the GeoJSON if (!map.getSource('geo_data')) { map.addSource('geo_data', { type: 'geojson', data: geo_data, cluster: true, clusterMaxZoom: 17, // Max zoom to cluster points on clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50) }); } // Custom point marker if ( map.listImages().includes("marker-icon") == false ) { var image = await map.loadImage('/marker.png'); map.addImage('marker-icon', image.data); } // Add the clusters if (!map.getLayer('clusters')) { map.addLayer({ id: 'clusters', type: 'circle', source: 'geo_data', filter: ['has', 'point_count'], paint: { // Use step expressions (https://maplibre.org/maplibre-style-spec/expressions/#step) // with three steps to implement three types of circles: // * Blue, 20px circles when point count is less than 100 // * Yellow, 30px circles when point count is between 100 and 750 // * Pink, 40px circles when point count is greater than or equal to 750 'circle-color': [ 'step', ['get', 'point_count'], '#51bbd655', 100, '#f1f07555', 750, '#f28cb155' ], 'circle-radius': [ 'step', ['get', 'point_count'], 20, 100, 30, 750, 40 ], 'circle-stroke-width': [ 'step', ['get', 'point_count'], 1, 100, 1, 750, 1 ], 'circle-stroke-color': [ 'step', ['get', 'point_count'], '#000', 100, '#000', 750, '#000' ], } }); // Show number of markers in each cluster map.addLayer({ id: 'cluster-count', type: 'symbol', source: 'geo_data', filter: ['has', 'point_count'], layout: { 'text-field': '{point_count_abbreviated}', 'text-font': ['Noto Sans Regular'], 'text-size': 25 } }); // Show individual markers map.addLayer({ id: 'unclustered-point', source: 'geo_data', filter: ['!', ['has', 'point_count']], type: 'symbol', layout: { "icon-overlap": "always", 'icon-image': 'marker-icon', // Use the PNG image 'icon-size': .1 // Adjust size if necessary } }); } } // Start by drawing the map map.on('load', async () => { await addCustomLayersAndSources(); });
Obviously, there's a lot more you can do - but I hope this shows just how quickly you can get a clustermap working on a globe.
More comments on Mastodon.