Drawing Route Elevation Profile with Chart.js

Grand Loop Road, Yellowstone National Park elevation profile
Grand Loop Road, Yellowstone National Park elevation profile

Route elevation data can help you determine a route's difficulty and estimate costs associated with the route. For example, the elevation profile allows you to see how steep the route is and whether it's suitable for hiking or cycling and classify it - easy, middle, difficult. You can also use elevations to estimate fuel consumption for vehicles when calculating routes for cars and trucks.

Geoapify Routing API can provide route elevation profiles with the results of a routing request. This tutorial shows you how to get route elevation data using Routing API and then display it on a chart using Chart.js. The JSFiddle code sample contains a working version of this tutorial.

Getting route elevation data

Geoapify Routing API can return many details about your route, such as surface type, speed limits, tolls, elevations, etc. To specify the list of required features, use the details parameter in the API URL. Here's an example of a route with elevation details:

https://api.geoapify.com/v1/routing?mode=bicycle&details=elevation&waypoints=44.568726,-110.386343|44.738832,-110.695645|44.509745,-110.848186|44.489782,-110.429748|44.675712,-110.7678004&apiKey=YOUR_API_KEY

In this URL example, you'll notice that you need to specify the details parameter as "elevation". Similarly, the mode parameter lets you set travel mode - car, motorcycle, bicycle, etc.

The Routing API returns elevation data and ranges for each route leg (a route between a pair of waypoints). Here is an example of elevation data for a route leg:

{
  "distance": 45521,
  "time": 9181.764,
  "steps": [
    {
      "from_index": 0,
      "to_index": 8,
      "distance": 139,
      "time": 36.028,
      "instruction": {
        "text": "Bike west on East Entrance Road."
      },
      "max_elevation": 2383,
      "min_elevation": 2377,
      "elevation_gain": 6,
      "elevation": 2380
    },
    {
      "from_index": 8,
      "to_index": 582,
      "distance": 24775,
      "time": 5120.704,
      "instruction": {
        "text": "Turn right onto Grand Loop Road."
      },
      "max_elevation": 2420,
      "min_elevation": 2340,
      "elevation_gain": 29,
      "elevation": 2363
    },
    ...
  ],
  "elevation_range": [[ 0, 2377 ], [ 21, 2378], [ 44, 2379 ], [ 68, 2380 ], [ 88, 2381 ], ... ],
  "elevation": [ 2377, 2378, 2379, 2380, ... ]
}

As you can see, the details=elevation parameter adds elevation, elevation gain, and maximum/minimum elevation values for each step. Also, an elevation profile for the route leg - elevation values corresponding for each geometry point. In addition, the elevation range array also includes a pair of [distance, elevation] values that will help you visualize the route elevation profile on a chart.

Let's explore the process of creating a route elevation profile chart with Chart.js.

Chart.js library for the road elevation profile

Chart.js is an excellent library that helps you make attractive charts in your JavaScript projects. The library is well documented, has a big community of programmers, and is under the commercial-products-friendly license.

You can install it through NPM or include a CDN in your HTML file to use this library:

Option 1. Install with NPM
npm install chart.js
Option 2. Add the library in HTML
<html>
  <head>
    ...
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
  </head>
  <body>...</body>
</html>

More detailed instructions for installation are on the Chart.js website.

To build the chart, you need to add a <canvas> element to your project:

<div class="elevation-profile-container" style="height:250px">
  <canvas id="route-elevation-chart" style="width:100%;height:100%"></canvas>
</div>

We'll utilize the <canvas> as a container for the chart.

Let's use a Line Chart to depict the route elevation. Here is an example of a line chart configuration that can be used for route profile visualization:

Chart.register( Chart.LineElement, Chart.LineController, Chart.Legend, Chart.Tooltip, Chart.LinearScale, Chart.PointElement, Chart.Filler, Chart.Title);

const ctx = document.getElementById("route-elevation-chart").getContext("2d");
const chartData = {
  labels: [0, 21, 44, 68, 88, 109, 125, 134, 139, 156, 164, 172, 187, 208, 263, 441, 472, 591, 664, 707, 785, 823, 900, 941, 1057, 1096, 1135, 1175, 1214], // this is test data
  datasets: [{
    data: [2377, 2378, 2379, 2380, 2381, 2382, 2383, 2383, 2383, 2384, 2384, 2384, 2384, 2384, 2388, 2391, 2392, 2393, 2392, 2391, 2394, 2395, 2397, 2397, 2394, 2393, 2394, 2393, 2392], // this is test 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: {
    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: { drawOnChartArea: false }},
    },
    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);

Here are some tips and tricks that you can use to make your Route Elevation Profile chart look better:

  • To make the chart responsive to window size change, set the option maintainAspectRatio to false.
  • Set the pointRadius to 0 to see the line only.
  • The example above shows you how to add the second y-axis - y1. You will need to adjust its range dynamically using the beforeInit plugin.
  • Use the tooltip plugin to generate a custom chart tooltip.

Visualize route elevation profile with Chart.js

The chart example from the previous step contains test data for a route elevation profile. Next, let's create a route elevation profile chart with the height data we've received from the Routing API.

Our Geoapify Routing API provides elevation ranges for every pair of waypoints. Here is an example of how you can compile the entire route elevation labels and data from the FeatureCollection object returned by the API:

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);
  ...
}, err => console.log(err));

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
  }
}

Please be aware that a route's geometry can include thousands of points. Just showing every single point on a chart can have some serious performance implications. That's why we suggest that you decrease the number of points and simplify the line on a chart.

There are various ways to simplify the line. This code sample shows the simplest one - you merely skip points that are not really different from the point you include before.

We showed how to create a new chart, but the chart can also get data updates. Follow the Updating Chart documentation page for detailed instructions.

Road Elevation map in JSFiddle code sample

We've created a demo of this tutorial to show you how it works. You can see the elevation profile map of a bike ride through Yellowstone National Park in the United States:

Switch to the JavaScript, HTML, and CSS tabs to see the code that makes this demo work.

Check out this complete example on JSFiddle >>

FAQ

What is a Route Elevation Profile?

The route elevation profile is a line graph illustrating elevation changes along the route. The x-axis represents distance, and the y-axis shows altitude above sea level. As a result, you can easily see the maximum and minimum elevations of the route and form an opinion about the route's difficulty.

How to get the elevation along a route with an API?

Geoapify Routing API returns the route elevation ranges as a result. You can calculate routes with heights for hiking, cycling, and driving modes. Here is JSFiddle showing how to get route elevations and create a route elevation profile.

How to get elevation of a cycling route with an API?

The Geoapify Routing API lets you build routes for city, mountain, and race bicycles. In addition, the API can return elevation data as part of a result route. Here is an example of a route elevation profile for a bicycle route through the Yellowstone National Park - JSFiddle >>

How to draw an Elevation Profile?

To draw a Route Elevation Profile, you can use any library or application that is able to create charts. For example, this JSFiddle demonstrates visualizing the Elevation Chart with Chart.js open-source library.

What's next