Show an Elevation Profile in Leaflet
This example shows a route on a Leaflet map with an interactive elevation profile. Hovering over the profile highlights the corresponding segment on the map and shows a position marker.
The elevation profile uses the MTK.ElevationProfile JavaScript class, loaded from the Maptoolkit CDN.
MTK.apiKey = "YOUR_API_KEY";
const polyline = [
[47.259163, 11.458077], [47.258754, 11.458337], [47.258611, 11.457979],
[47.258567, 11.457556], [47.258058, 11.456872], [47.257937, 11.456416],
[47.257837, 11.456319], [47.257638, 11.456384], [47.257362, 11.455895],
[47.257307, 11.455407], [47.257318, 11.454625], [47.257196, 11.454235],
[47.256544, 11.454121], [47.255826, 11.454235], [47.255473, 11.454511],
[47.255285, 11.454544], [47.255152, 11.454544], [47.254953, 11.454886],
[47.254876, 11.455097], [47.254843, 11.455097], [47.254953, 11.454495],
[47.254169, 11.454153], [47.253240, 11.452851], [47.252898, 11.450978],
[47.252666, 11.450571], [47.252279, 11.450083], [47.252012, 11.449057],
[47.251890, 11.448096], [47.251503, 11.447429],
];
const map = L.map("map").setView([47.255333, 11.452883], 15);
L.tileLayer(`https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{ratio}.png?api_key=${MTK.apiKey}`, {
ratio: L.Browser.retina ? "@2x" : "", maxZoom: 18,
attribution: "© <a href='https://www.maptoolkit.com'>Maptoolkit</a> © <a href='https://www.openstreetmap.org/copyright'>OSM</a>",
}).addTo(map);
new L.Polyline(polyline, { color: "#2a3561", weight: 5 }).addTo(map);
const highlight = new L.Polyline([], { color: "#fa3f38", weight: 5 }).addTo(map);
const marker = new L.Marker([0, 0], {
icon: new L.Icon({ iconUrl: "https://static.maptoolkit.net/sprites/toursprung/marker.svg", iconSize: [30, 29], iconAnchor: [15, 29] }),
});
const elevation = new MTK.ElevationProfile().addTo("profile").setLngLats(polyline.map(([lat, lng]) => [lng, lat]));
elevation.on("mousemove", (ev) => { !marker._map && marker.addTo(map); marker.setLatLng([ev.lngLat[1], ev.lngLat[0]]); });
elevation.on("mouseout", () => { marker._map && marker.remove(); });
elevation.on("highlight", (ev) => { highlight.setLatLngs(ev.lngLats.map(([lng, lat]) => [lat, lng])); });<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" />
<link rel="stylesheet" href="https://static.maptoolkit.net/mtk/apis/v1.1/elevation.css" />
<style>
html, body { width: 100%; height: 100%; padding: 0; margin: 0; }
#map { width: 100%; height: 100%; }
#profile { position: absolute; bottom: 0; right: 0; width: 500px; margin: 0 10px 30px 0; z-index: 999; }
#profile .mtk-elevation-profile { background: #fff; box-shadow: 0 0 15px #68686880; border-radius: 6px; }
#profile .mtk-elevation-curve-stroke { stroke: #303f7e; stroke-width: 1rem; stroke-opacity: 1; }
#profile .mtk-elevation-curve-fill { fill: #303f7e; }
#profile .mtk-elevation-curve-bar { stroke: #fa3f38; }
#profile .mtk-elevation-section { fill: rgba(250, 63, 56, 0.3); }
</style>
</head>
<body>
<div id="map"></div>
<div id="profile"></div>
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"></script>
<script src="https://static.maptoolkit.net/mtk/apis/v1.1/elevation.js?api_key=YOUR_API_KEY"></script>
<script>
MTK.apiKey = "YOUR_API_KEY";
const polyline = [
[47.259163, 11.458077], [47.258754, 11.458337], [47.258611, 11.457979],
[47.258567, 11.457556], [47.258058, 11.456872], [47.257937, 11.456416],
[47.257837, 11.456319], [47.257638, 11.456384], [47.257362, 11.455895],
[47.257307, 11.455407], [47.257318, 11.454625], [47.257196, 11.454235],
[47.256544, 11.454121], [47.255826, 11.454235], [47.255473, 11.454511],
[47.255285, 11.454544], [47.255152, 11.454544], [47.254953, 11.454886],
[47.254876, 11.455097], [47.254843, 11.455097], [47.254953, 11.454495],
[47.254169, 11.454153], [47.253240, 11.452851], [47.252898, 11.450978],
[47.252666, 11.450571], [47.252279, 11.450083], [47.252012, 11.449057],
[47.251890, 11.448096], [47.251503, 11.447429],
];
const map = L.map("map").setView([47.255333, 11.452883], 15);
L.tileLayer(`https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{ratio}.png?api_key=${MTK.apiKey}`, {
ratio: L.Browser.retina ? "@2x" : "", maxZoom: 18,
attribution: "© <a href='https://www.maptoolkit.com'>Maptoolkit</a> © <a href='https://www.openstreetmap.org/copyright'>OSM</a>",
}).addTo(map);
new L.Polyline(polyline, { color: "#2a3561", weight: 5 }).addTo(map);
const highlight = new L.Polyline([], { color: "#fa3f38", weight: 5 }).addTo(map);
const marker = new L.Marker([0, 0], {
icon: new L.Icon({ iconUrl: "https://static.maptoolkit.net/sprites/toursprung/marker.svg", iconSize: [30, 29], iconAnchor: [15, 29] }),
});
const elevation = new MTK.ElevationProfile().addTo("profile").setLngLats(polyline.map(([lat, lng]) => [lng, lat]));
elevation.on("mousemove", (ev) => { !marker._map && marker.addTo(map); marker.setLatLng([ev.lngLat[1], ev.lngLat[0]]); });
elevation.on("mouseout", () => { marker._map && marker.remove(); });
elevation.on("highlight", (ev) => { highlight.setLatLngs(ev.lngLats.map(([lng, lat]) => [lat, lng])); });
</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
This example loads two Maptoolkit libraries, and they authorize separately. The tile
layer carries api_key in its URL, while elevation.js reads the global MTK.apiKey. Set
one and not the other and either the map or the profile comes back empty.
MTK.ElevationProfile fetches elevations for the coordinates you give it and draws the
chart, then emits events you wire to the map: mousemove for the cursor position,
highlight for a selected stretch, mouseout when the cursor leaves.
Coordinates here are [lat, lng], matching Leaflet. The MapLibre version of this example
uses [lng, lat] for the same route, which is worth noting if you copy between them.
The route is a fixed coordinate array. In a real application it would come from the Routing API, whose encoded polyline you decode first.
Next steps
The profile is drawn from a fixed line here, so the first real step is feeding it a route someone chose. The Routing API returns that geometry, and the two wired together are the standard outdoor pairing: pick a route, see the climb.
From the same data you can derive the numbers people actually compare: total ascent and descent, steepest section, height gained per kilometre. Those come from the elevation array rather than from another request, and the Elevation API reference covers the single-point and batch shapes for anything the profile does not cover.