Show Polygon Information on Click in Maptoolkit Maps JS
Showing feature data on polygon click requires listening to the map’s click event on a fill layer and reading the properties of the first returned feature. A Popup is then placed at the click coordinates with the feature properties formatted as HTML. This example loads Austria’s federal-state boundaries from a public GeoJSON dataset, groups them into Western and Eastern Austria with one color each, and shows the region name, area, and capital on click. Use this pattern for choropleth maps, administrative boundary explorers, or any map where polygon features carry data that users should be able to inspect by clicking.
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [13.35, 47.6],
zoom: 6.4,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
// Austria's nine federal states, grouped into West (capital Innsbruck) and East (capital Vienna).
const WEST_STATES = ['Tirol', 'Vorarlberg', 'Salzburg'];
// Geodesic area (m^2) for Polygon / MultiPolygon rings (spherical excess, like turf.area).
function ringArea(ring) {
const R = 6378137;
let total = 0;
for (let i = 0; i < ring.length - 1; i++) {
const [lng1, lat1] = ring[i];
const [lng2, lat2] = ring[i + 1];
total += (lng2 - lng1) * Math.PI / 180 *
(2 + Math.sin(lat1 * Math.PI / 180) + Math.sin(lat2 * Math.PI / 180));
}
return Math.abs(total * R * R / 2);
}
function polygonArea(rings) {
let area = ringArea(rings[0]);
for (let i = 1; i < rings.length; i++) area -= ringArea(rings[i]); // subtract holes
return area;
}
function featureArea(geometry) {
if (geometry.type === 'MultiPolygon') {
return geometry.coordinates.reduce((sum, poly) => sum + polygonArea(poly), 0);
}
return polygonArea(geometry.coordinates);
}
map.on('load', async () => {
// Administrative boundaries: public dataset (Statistik Austria, via the
// ginseng666/GeoJSON-TopoJSON-Austria project), served over the jsDelivr CDN.
const response = await fetch('https://cdn.jsdelivr.net/gh/ginseng666/GeoJSON-TopoJSON-Austria@master/2021/simplified-99.9/laender_999_geo.json');
const states = await response.json();
// Tag each state with its group, then total each group's area.
const groupArea = { West: 0, East: 0 };
states.features.forEach((feature) => {
const group = WEST_STATES.includes(feature.properties.name) ? 'West' : 'East';
feature.properties.group = group;
feature.properties.region = group === 'West' ? 'Western Austria' : 'Eastern Austria';
feature.properties.capital = group === 'West' ? 'Innsbruck' : 'Vienna';
groupArea[group] += featureArea(feature.geometry);
});
states.features.forEach((feature) => {
feature.properties.regionArea = Math.round(groupArea[feature.properties.group] / 1e6).toLocaleString() + ' km²';
});
map.addSource('states', { type: 'geojson', data: states });
// One color per group: West vs East.
map.addLayer({
id: 'states-fill',
type: 'fill',
source: 'states',
paint: {
'fill-color': ['match', ['get', 'group'], 'West', '#2c7fb8', 'East', '#de2d26', '#888888'],
'fill-opacity': 0.45
}
});
map.addLayer({
id: 'states-border',
type: 'line',
source: 'states',
paint: { 'line-color': '#ffffff', 'line-width': 1 }
});
map.fitBounds([[9.45, 46.35], [17.2, 49.1]], { padding: 30, duration: 0 });
map.on('click', 'states-fill', (e) => {
const p = e.features[0].properties;
new maptoolkit.Popup()
.setLngLat(e.lngLat)
.setHTML(`<strong>${p.region}</strong><br>State: ${p.name}<br>Region area: ${p.regionArea}<br>Capital: ${p.capital}`)
.addTo(map);
});
map.on('mouseenter', 'states-fill', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'states-fill', () => { map.getCanvas().style.cursor = ''; });
});<!DOCTYPE html>
<html lang="en">
<head>
<title>Show Polygon Information on Click - Maptoolkit Maps JS</title>
<meta property="og:description" content="Show a popup with feature properties when clicking on a polygon." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [13.35, 47.6],
zoom: 6.4,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
// Austria's nine federal states, grouped into West (capital Innsbruck) and East (capital Vienna).
const WEST_STATES = ['Tirol', 'Vorarlberg', 'Salzburg'];
// Geodesic area (m^2) for Polygon / MultiPolygon rings (spherical excess, like turf.area).
function ringArea(ring) {
const R = 6378137;
let total = 0;
for (let i = 0; i < ring.length - 1; i++) {
const [lng1, lat1] = ring[i];
const [lng2, lat2] = ring[i + 1];
total += (lng2 - lng1) * Math.PI / 180 *
(2 + Math.sin(lat1 * Math.PI / 180) + Math.sin(lat2 * Math.PI / 180));
}
return Math.abs(total * R * R / 2);
}
function polygonArea(rings) {
let area = ringArea(rings[0]);
for (let i = 1; i < rings.length; i++) area -= ringArea(rings[i]); // subtract holes
return area;
}
function featureArea(geometry) {
if (geometry.type === 'MultiPolygon') {
return geometry.coordinates.reduce((sum, poly) => sum + polygonArea(poly), 0);
}
return polygonArea(geometry.coordinates);
}
map.on('load', async () => {
// Administrative boundaries: public dataset (Statistik Austria, via the
// ginseng666/GeoJSON-TopoJSON-Austria project), served over the jsDelivr CDN.
const response = await fetch('https://cdn.jsdelivr.net/gh/ginseng666/GeoJSON-TopoJSON-Austria@master/2021/simplified-99.9/laender_999_geo.json');
const states = await response.json();
// Tag each state with its group, then total each group's area.
const groupArea = { West: 0, East: 0 };
states.features.forEach((feature) => {
const group = WEST_STATES.includes(feature.properties.name) ? 'West' : 'East';
feature.properties.group = group;
feature.properties.region = group === 'West' ? 'Western Austria' : 'Eastern Austria';
feature.properties.capital = group === 'West' ? 'Innsbruck' : 'Vienna';
groupArea[group] += featureArea(feature.geometry);
});
states.features.forEach((feature) => {
feature.properties.regionArea = Math.round(groupArea[feature.properties.group] / 1e6).toLocaleString() + ' km²';
});
map.addSource('states', { type: 'geojson', data: states });
// One color per group: West vs East.
map.addLayer({
id: 'states-fill',
type: 'fill',
source: 'states',
paint: {
'fill-color': ['match', ['get', 'group'], 'West', '#2c7fb8', 'East', '#de2d26', '#888888'],
'fill-opacity': 0.45
}
});
map.addLayer({
id: 'states-border',
type: 'line',
source: 'states',
paint: { 'line-color': '#ffffff', 'line-width': 1 }
});
map.fitBounds([[9.45, 46.35], [17.2, 49.1]], { padding: 30, duration: 0 });
map.on('click', 'states-fill', (e) => {
const p = e.features[0].properties;
new maptoolkit.Popup()
.setLngLat(e.lngLat)
.setHTML(`<strong>${p.region}</strong><br>State: ${p.name}<br>Region area: ${p.regionArea}<br>Capital: ${p.capital}`)
.addTo(map);
});
map.on('mouseenter', 'states-fill', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'states-fill', () => { map.getCanvas().style.cursor = ''; });
});
</script>
</body>
</html>Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.
How it works
The click handler is registered against the fill layer, so e.features contains only
polygons from that layer and e.features[0].properties is the data behind the one clicked.
The popup is placed at e.lngLat, the click position, rather than at the polygon’s centre.
For a large or awkwardly shaped state a centroid can fall outside the polygon entirely, so
the click point is usually the better anchor.
The area calculation is worth noticing: it is geodesic, computed on the sphere with the
same spherical-excess approach as turf.area, not a planar area from the raw coordinates. A
planar calculation over degrees is wrong by a factor that grows with latitude, so Austria
would come out noticeably too large.
properties values arrive as strings when they came from vector tiles, so numbers usually
need coercing before formatting.
Next steps
One polygon’s data is a start; comparing them is usually the point. Colouring the fill from the same property you are reading turns the map into a choropleth, so the pattern is visible before anyone clicks.
Joining the tile features to your own data by name or code is the step after that, and it is where most of the work lands: the map supplies the shapes, your database supplies the numbers, and Connectors are how the two meet once the join is too large for the browser.