Skip to content

Show an Elevation Profile in MapLibre GL JS

This example shows a route on a MapLibre GL map with an interactive elevation profile. Hovering over the profile highlights the corresponding segment on the map and shows a position marker.

MTK.apiKey = "YOUR_API_KEY";
    const polyline = [
      [11.458077, 47.259163], [11.458337, 47.258754], [11.457979, 47.258611],
      [11.457556, 47.258567], [11.456872, 47.258058], [11.456416, 47.257937],
      [11.456319, 47.257837], [11.456384, 47.257638], [11.455895, 47.257362],
      [11.455407, 47.257307], [11.454625, 47.257318], [11.454235, 47.257196],
      [11.454121, 47.256544], [11.454235, 47.255826], [11.454511, 47.255473],
      [11.454544, 47.255285], [11.454544, 47.255152], [11.454886, 47.254953],
      [11.455097, 47.254876], [11.455097, 47.254843], [11.454495, 47.254953],
      [11.454153, 47.254169], [11.452851, 47.253240], [11.450978, 47.252898],
      [11.450571, 47.252666], [11.450083, 47.252279], [11.449057, 47.252012],
      [11.448096, 47.251890], [11.447429, 47.251503],
    ];
    const map = new maplibregl.Map({
      container: "map",
      style: "https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=YOUR_API_KEY",
      center: [11.452883, 47.255333],
      zoom: 14,
    });
    map.once("load", () => {
      map.addLayer({ id: "route", type: "line", source: { type: "geojson", data: { type: "Feature", geometry: { type: "LineString", coordinates: polyline } } }, layout: { "line-join": "round", "line-cap": "round" }, paint: { "line-color": "#2a3561", "line-width": 5 } });
      map.addLayer({ id: "highlight", type: "line", source: { type: "geojson", data: { type: "Feature", geometry: { type: "LineString", coordinates: [] } } }, layout: { "line-join": "round", "line-cap": "round" }, paint: { "line-color": "#fa3f38", "line-width": 5 } });
      const img = document.createElement("img");
      img.src = "https://static.maptoolkit.net/sprites/toursprung/marker.svg";
      img.width = 29; img.height = 30;
      const marker = new maplibregl.Marker({ element: img, anchor: "bottom" }).setLngLat([0, 0]);
      const elevation = new MTK.ElevationProfile().addTo("profile").setLngLats(polyline);
      elevation.on("mousemove", (ev) => { !marker._map && marker.addTo(map); marker.setLngLat(ev.lngLat); });
      elevation.on("mouseout", () => { marker._map && marker.remove(); });
      elevation.on("highlight", (ev) => { map.getSource("highlight").setData({ type: "Feature", geometry: { type: "LineString", coordinates: ev.lngLats } }); });
    });
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.css" />
  <link rel="stylesheet" href="https://static.maptoolkit.net/mtk/apis/v1.1/elevation.css" />
  <style>
    html, body { width: 100%; height: 100%; margin: 0; padding: 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://static.maptoolkit.net/mtk/apis/v1.1/elevation.js?api_key=YOUR_API_KEY"></script>
  <script>
    MTK.apiKey = "YOUR_API_KEY";
    const polyline = [
      [11.458077, 47.259163], [11.458337, 47.258754], [11.457979, 47.258611],
      [11.457556, 47.258567], [11.456872, 47.258058], [11.456416, 47.257937],
      [11.456319, 47.257837], [11.456384, 47.257638], [11.455895, 47.257362],
      [11.455407, 47.257307], [11.454625, 47.257318], [11.454235, 47.257196],
      [11.454121, 47.256544], [11.454235, 47.255826], [11.454511, 47.255473],
      [11.454544, 47.255285], [11.454544, 47.255152], [11.454886, 47.254953],
      [11.455097, 47.254876], [11.455097, 47.254843], [11.454495, 47.254953],
      [11.454153, 47.254169], [11.452851, 47.253240], [11.450978, 47.252898],
      [11.450571, 47.252666], [11.450083, 47.252279], [11.449057, 47.252012],
      [11.448096, 47.251890], [11.447429, 47.251503],
    ];
    const map = new maplibregl.Map({
      container: "map",
      style: "https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=YOUR_API_KEY",
      center: [11.452883, 47.255333],
      zoom: 14,
    });
    map.once("load", () => {
      map.addLayer({ id: "route", type: "line", source: { type: "geojson", data: { type: "Feature", geometry: { type: "LineString", coordinates: polyline } } }, layout: { "line-join": "round", "line-cap": "round" }, paint: { "line-color": "#2a3561", "line-width": 5 } });
      map.addLayer({ id: "highlight", type: "line", source: { type: "geojson", data: { type: "Feature", geometry: { type: "LineString", coordinates: [] } } }, layout: { "line-join": "round", "line-cap": "round" }, paint: { "line-color": "#fa3f38", "line-width": 5 } });
      const img = document.createElement("img");
      img.src = "https://static.maptoolkit.net/sprites/toursprung/marker.svg";
      img.width = 29; img.height = 30;
      const marker = new maplibregl.Marker({ element: img, anchor: "bottom" }).setLngLat([0, 0]);
      const elevation = new MTK.ElevationProfile().addTo("profile").setLngLats(polyline);
      elevation.on("mousemove", (ev) => { !marker._map && marker.addTo(map); marker.setLngLat(ev.lngLat); });
      elevation.on("mouseout", () => { marker._map && marker.remove(); });
      elevation.on("highlight", (ev) => { map.getSource("highlight").setData({ type: "Feature", geometry: { type: "LineString", coordinates: ev.lngLats } }); });
    });
  </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.

Use the Maptoolkit Connector. Use MapLibre GL. Use this Polyline to create a route and an elevation profile: [11.458077, 47.259163],[11.458337, 47.258754],[11.457979, 47.258611],[11.457556, 47.258567],[11.456872, 47.258058],[11.456416, 47.257937],[11.456319, 47.257837],[11.456384, 47.257638],[11.455895, 47.257362],[11.455407, 47.257307],[11.454625, 47.257318],[11.454235, 47.257196],[11.454121, 47.256544],[11.454235, 47.255826],[11.454511, 47.255473],[11.454544, 47.255285],[11.454544, 47.255152],[11.454886, 47.254953],[11.455097, 47.254876],[11.455097, 47.254843],[11.454495, 47.254953],[11.454153, 47.254169],[11.452851, 47.253240],[11.450978, 47.252898],[11.450571, 47.252666],[11.450083, 47.252279],[11.449057, 47.252012],[11.448096, 47.251890],[11.447429, 47.251503].

How it works

This example loads two Maptoolkit libraries, and they authorize separately. The map style 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 [lng, lat], matching MapLibre. The Leaflet version of this example uses [lat, lng] 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 line here is fixed, so the first step is feeding the profile a route the user chose. The Routing API returns it, and the pairing is the backbone of most outdoor applications: pick a route, see the climb.

Total ascent, steepest section and metres gained per kilometre all fall out of the elevation array without another request, and the Elevation API reference covers the single-point and batch shapes for anything else. Showing relief under the route makes the profile easier to trust, which is what Terrain Tiles are for.