Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save iamgeoknight/2813898d05d4e219e1ee95e0ef012e73 to your computer and use it in GitHub Desktop.

Select an option

Save iamgeoknight/2813898d05d4e219e1ee95e0ef012e73 to your computer and use it in GitHub Desktop.
OpenLayers Clustering Tutorial: 100000 Points Without Browser Freeze (HTML + JavaScript)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OpenLayers 50,000 Point Clustering Without Freeze</title>
<!-- OpenLayers CSS -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/ol@v10.9.0/ol.css"
/>
<!-- OpenLayers JS full build -->
<script src="https://cdn.jsdelivr.net/npm/ol@v10.9.0/dist/ol.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: Arial, sans-serif;
}
#map {
width: 100%;
height: 100vh;
}
#infoPanel {
position: absolute;
top: 12px;
left: 12px;
z-index: 1000;
background: white;
padding: 14px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
font-size: 14px;
min-width: 280px;
}
#infoPanel h3 {
margin: 0 0 10px 0;
font-size: 16px;
}
.row {
margin-bottom: 8px;
}
label {
display: block;
margin-top: 10px;
}
input[type="range"] {
width: 100%;
}
.small {
font-size: 12px;
color: #555;
line-height: 1.4;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="infoPanel">
<h3>OpenLayers 100,000 Point Cluster</h3>
<div class="row">
Raw points:
<b id="rawCount">0</b>
</div>
<div class="row">
Current cluster features:
<b id="clusterCount">0</b>
</div>
<div class="row">
Current zoom:
<b id="zoomValue">0</b>
</div>
<label>
Cluster distance:
<b id="distanceValue">40</b> px
<input
id="distanceSlider"
type="range"
min="10"
max="120"
value="40"
/>
</label>
<p class="small">
Larger distance = fewer visible cluster symbols.<br />
Smaller distance = more detailed clustering.
</p>
</div>
<script>
/************************************************************
* 1. Basic configuration
************************************************************/
const TOTAL_POINTS = 100000;
const rawCountEl = document.getElementById("rawCount");
const clusterCountEl = document.getElementById("clusterCount");
const zoomValueEl = document.getElementById("zoomValue");
const distanceSlider = document.getElementById("distanceSlider");
const distanceValueEl = document.getElementById("distanceValue");
rawCountEl.textContent = TOTAL_POINTS.toLocaleString();
/************************************************************
* 2. Create random points
*
* For demo, we generate random points inside India-like bounds.
* In real projects, these points may come from GeoJSON,
* PostGIS, API, CSV, or database.
************************************************************/
const minLon = 68.0;
const maxLon = 98.0;
const minLat = 6.0;
const maxLat = 36.0;
function randomBetween(min, max) {
return min + Math.random() * (max - min);
}
function createRandomFeatures(count) {
const features = new Array(count);
for (let i = 0; i < count; i++) {
const lon = randomBetween(minLon, maxLon);
const lat = randomBetween(minLat, maxLat);
const coordinate3857 = ol.proj.fromLonLat([lon, lat]);
const feature = new ol.Feature({
geometry: new ol.geom.Point(coordinate3857),
pointId: i
});
features[i] = feature;
}
return features;
}
/*
Important performance idea:
Create all features first.
Then give the whole feature array to VectorSource.
Avoid doing source.addFeature(feature) 50,000 times.
*/
const pointFeatures = createRandomFeatures(TOTAL_POINTS);
/************************************************************
* 3. Raw point source
************************************************************/
const pointSource = new ol.source.Vector({
features: pointFeatures
});
/************************************************************
* 4. Cluster source
*
* distance:
* Pixel distance within which points are grouped.
*
* minDistance:
* Minimum pixel distance between clusters.
* OpenLayers says minDistance is capped by distance.
************************************************************/
const clusterSource = new ol.source.Cluster({
distance: Number(distanceSlider.value),
minDistance: 10,
source: pointSource
});
/************************************************************
* 5. Cached cluster styles
*
* This is very important.
*
* Do NOT create a new style object every time OpenLayers renders.
* Cache style objects by cluster size group.
************************************************************/
const styleCache = {};
function getClusterStyle(clusterFeature) {
const children = clusterFeature.get("features");
const size = children.length;
let radius;
let fontSize;
if (size >= 1000) {
radius = 26;
fontSize = 13;
} else if (size >= 500) {
radius = 23;
fontSize = 13;
} else if (size >= 100) {
radius = 20;
fontSize = 12;
} else if (size >= 10) {
radius = 16;
fontSize = 12;
} else {
radius = 10;
fontSize = 11;
}
/*
We cache by visual bucket, not by exact size only.
This reduces repeated Style object creation.
*/
const cacheKey = `${radius}-${fontSize}-${size}`;
if (!styleCache[cacheKey]) {
styleCache[cacheKey] = new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
fill: new ol.style.Fill({
color: "rgba(0, 136, 255, 0.75)"
}),
stroke: new ol.style.Stroke({
color: "rgba(255, 255, 255, 0.95)",
width: 3
})
}),
text: new ol.style.Text({
text: size.toString(),
font: `bold ${fontSize}px Arial`,
fill: new ol.style.Fill({
color: "#ffffff"
}),
stroke: new ol.style.Stroke({
color: "rgba(0, 0, 0, 0.45)",
width: 3
})
})
});
}
return styleCache[cacheKey];
}
/************************************************************
* 6. Cluster layer
*
* updateWhileAnimating and updateWhileInteracting are kept false.
*
* OpenLayers documents that enabling updateWhileAnimating
* recreates feature batches during animations and has a
* performance impact for large vector datasets.
************************************************************/
const clusterLayer = new ol.layer.Vector({
source: clusterSource,
style: getClusterStyle,
updateWhileAnimating: false,
updateWhileInteracting: false
});
/************************************************************
* 7. Base map layer
************************************************************/
const osmLayer = new ol.layer.Tile({
source: new ol.source.OSM()
});
/************************************************************
* 8. Create map
************************************************************/
const map = new ol.Map({
target: "map",
layers: [
osmLayer,
clusterLayer
],
view: new ol.View({
center: ol.proj.fromLonLat([78.9629, 20.5937]),
zoom: 5
})
});
/************************************************************
* 9. Update panel statistics
************************************************************/
function updateStats() {
const zoom = map.getView().getZoom();
const clusterFeatures = clusterSource.getFeatures();
zoomValueEl.textContent = zoom.toFixed(2);
clusterCountEl.textContent = clusterFeatures.length.toLocaleString();
}
map.on("moveend", updateStats);
clusterSource.on("change", updateStats);
/************************************************************
* 10. Change cluster distance dynamically
************************************************************/
distanceSlider.addEventListener("input", function () {
const distance = Number(distanceSlider.value);
distanceValueEl.textContent = distance;
/*
OpenLayers Cluster source supports setDistance().
*/
clusterSource.setDistance(distance);
updateStats();
});
/************************************************************
* 11. Click cluster and zoom to its children
************************************************************/
map.on("click", function (event) {
clusterLayer.getFeatures(event.pixel).then(function (clickedFeatures) {
if (!clickedFeatures.length) {
return;
}
const clusterFeature = clickedFeatures[0];
const children = clusterFeature.get("features");
if (!children || children.length === 0) {
return;
}
/*
If this cluster contains only one point, show its info.
*/
if (children.length === 1) {
const originalFeature = children[0];
console.log("Single point clicked:", originalFeature.get("pointId"));
return;
}
/*
If cluster contains many points, zoom to their extent.
*/
const coordinates = children.map(function (feature) {
return feature.getGeometry().getCoordinates();
});
const extent = ol.extent.boundingExtent(coordinates);
map.getView().fit(extent, {
duration: 400,
padding: [80, 80, 80, 80],
maxZoom: 16
});
});
});
updateStats();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment