Chart.register( Chart.LineElement, Chart.LineController, Chart.Legend, Chart.Tooltip, Chart.LinearScale, Chart.PointElement, Chart.Filler, Chart.Title ); // The API Key provided is restricted to JSFiddle website // Get your own API Key on https://myprojects.geoapify.com const myAPIKey = "6dc7fb95a3b246cfa0f3bcef5ce9ed9a"; const map = new maplibregl.Map({ container: 'my-map', style: `https://maps.geoapify.com/v1/styles/osm-carto/style.json?apiKey=${myAPIKey}`, center: [-110.63886603832373, 44.57344946153063], zoom: 8 }); map.addControl(new maplibregl.NavigationControl()); const popup = new maplibregl.Popup(); const waypoints = [{ latlon: [44.56887641018278, -110.37193509232105], address: "Howard Eaton-Fishing Bridge-Canyon, Park County, WY, United States of America" }, { latlon: [44.64991504629589, -110.87685585784652], address: "251 Echo Canyon Road, Teton County, WY, United States of America" }, { latlon: [44.46198969253814, -110.83290070191913], address: "Lower Hamilton Store, 251 Echo Canyon Road, Teton County, WY, United States of America" }, { latlon: [44.534340496926745, -110.43392313273148], address: "Grand Loop Road, Bridge Bay, WY, United States of America" } ] // create markers const markers = []; waypoints.forEach(waypoint => { markers.push(new maplibregl.Marker().setLngLat([waypoint.latlon[1], waypoint.latlon[0]]) .setPopup(new maplibregl.Popup().setText(waypoint.address)).addTo(map)); }); let routeData; let elevationData; fetch(`https://api.geoapify.com/v1/routing?waypoints=${waypoints.map(waypoint => waypoint.latlon.join(',')).join('|')}&mode=mountain_bike&details=elevation&apiKey=${myAPIKey}`).then(res => res.json()).then(routeResult => { routeData = routeResult; elevationData = calculateElevationProfileData(routeResult); map.addSource('route', { type: 'geojson', data: routeData }); drawRoute(); drawElevationProfile(); }, err => console.log(err)); function drawRoute() { if (!routeData) { return; } if (map.getLayer('route-layer')) { map.removeLayer('route-layer') } map.getSource('route').setData(routeData); map.addLayer({ 'id': 'route-layer', 'type': 'line', 'source': 'route', 'layout': { 'line-cap': "round", 'line-join': "round" }, 'paint': { 'line-color': "#6084eb", 'line-width': 8 }, 'filter': ['==', '$type', 'LineString'] }); } function calculateElevationProfileData(routeData) { const legElevations = []; // elevation_range contains pairs [distance, elevation] for every leg geometry point routeData.features[0].properties.legs.forEach(leg => { if (leg.elevation_range) { legElevations.push(leg.elevation_range); } else { legElevations.push([]); } }); labels = []; data = []; legElevations.forEach((legElevation, index) => { let previousLegsDistance = 0; for (let i = 0; i <= index - 1; i++) { previousLegsDistance += legElevations[i][legElevations[i].length - 1][0]; } labels.push(...legElevation.map(elevationData => elevationData[0] + previousLegsDistance)); data.push(...legElevation.map(elevationData => elevationData[1])); }); // optimize array size to avoid performance problems const labelsOptimized = []; const dataOptimized = []; const minDist = 5; // 5m const minHeight = 10; // ~10m labels.forEach((dist, index) => { if (index === 0 || index === labels.length - 1 || (dist - labelsOptimized[labelsOptimized.length - 1]) > minDist || Math.abs(data[index] - dataOptimized[dataOptimized.length - 1]) > minHeight) { labelsOptimized.push(dist); dataOptimized.push(data[index]); } }); return { data: dataOptimized, labels: labelsOptimized } } function drawElevationProfile() { const ctx = document.getElementById("route-elevation-chart").getContext("2d"); const chartData = { labels: elevationData.labels, datasets: [{ data: elevationData.data, fill: true, borderColor: '#66ccff', backgroundColor: '#66ccff66', tension: 0.1, pointRadius: 0, spanGaps: true }] }; const config = { type: 'line', data: chartData, plugins: [{ beforeInit: (chart, args, options) => { const maxHeight = Math.max(...chart.data.datasets[0].data); chart.options.scales.x.min = Math.min(...chart.data.labels); chart.options.scales.x.max = Math.max(...chart.data.labels); chart.options.scales.y.max = maxHeight + Math.round(maxHeight * 0.2); chart.options.scales.y1.max = maxHeight + Math.round(maxHeight * 0.2); } }], options: { onHover: function(e, item) { // add hover here!!! }, animation: false, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index', }, tooltip: { position: 'nearest' }, scales: { x: { type: 'linear' }, y: { type: 'linear', beginAtZero: true }, y1: { type: 'linear', display: true, position: 'right', beginAtZero: true, // grid line settings grid: { drawOnChartArea: false, // only want the grid lines for one axis to show up }, }, }, plugins: { title: { align: "end", display: true, text: "Distance, m / Elevation, m" }, legend: { display: false }, tooltip: { displayColors: false, callbacks: { title: (tooltipItems) => { return "Distance: " + tooltipItems[0].label + 'm' }, label: (tooltipItem) => { return "Elevation: " + tooltipItem.raw + 'm' }, } } } } }; const chart = new Chart(ctx, config); }
<div id="my-map"></div> <div class="elevation-profile-container"> <canvas id="route-elevation-chart" style="width:100%;height:100%"></canvas> </div>
html, body { margin: 0; padding: 0; height: 100%; display: flex; flex-direction: column; } #my-map { flex: 1; } .elevation-profile-container { height: 250px; }