DEV Community

Cover image for How to Create a Route Elevation Profile with Chart.js and Geoapify Routing API
Casey Rivers for Geoapify Maps API

Posted on

How to Create a Route Elevation Profile with Chart.js and Geoapify Routing API

This is the fourth tutorial in our series on building printable route directions. The full example is available on GitHub:

👉 Printable route directions example

In the previous tutorials, we covered requesting routes and directions, generating route overview images, and creating step preview maps. Now we add the final piece: elevation profiles.

Elevation data transforms a flat route into something more meaningful. A 10 km hike looks very different if it's flat vs. climbing 500 meters. Cyclists care about this, hikers care about this, and even drivers benefit from knowing about steep grades that affect fuel consumption or travel time.

When we built the printable directions demo, we wanted to include an elevation chart that gives users a quick visual sense of the terrain. In this tutorial, you'll learn how to extract elevation data from the Routing API and render it as a clean, responsive chart using Chart.js.

Try the live demo:

âžĄī¸ View on CodePen

APIs and Libraries:

What you will learn:

  • Request elevation data from the Routing API
  • Extract elevation points from the response
  • Optimize data for chart performance
  • Render a smooth elevation profile with Chart.js

Table of Contents

  1. Request Elevation Data
  2. Extract Elevation Points
  3. Optimize the Data
  4. Render with Chart.js
  5. Explore the Demo
  6. Summary
  7. FAQ

Step 1: Request Elevation Data

The Routing API includes elevation data when you add details=elevation to the request. This is an optional detail. If you don't need elevation, skip it to reduce response size.

const waypoints = "48.1351,11.5820|52.5200,13.4050"; // Munich to Berlin

// Add details=elevation to get elevation data
const routingUrl = `https://api.geoapify.com/v1/routing?waypoints=${waypoints}&mode=drive&details=elevation&apiKey=${apiKey}`;

const response = await fetch(routingUrl);
const data = await response.json();
const route = data.features[0];
Enter fullscreen mode Exit fullscreen mode

The response includes elevation_range in each leg:

{
  "features": [{
    "properties": {
      "distance": 585050,
      "time": 20454.257,
      "legs": [{
        "elevation_range": [
          [0, 513.7],                    // [distance in meters, elevation in meters]
          [7.17, 513.98],
          [23.15, 514.62],
          [31.12, 515.02],
          // ... thousands more points ...
          [584383.17, 39.32]
        ]
      }]
    }
  }]
}
Enter fullscreen mode Exit fullscreen mode

Each elevation_range array contains [distance, elevation] pairs for points along the route.

â„šī¸ Combining details

You can request multiple details in one call: details=elevation,instruction_details. This is more efficient than making separate requests if you need both elevation and turn-by-turn instructions.


Step 2: Extract Elevation Points

Loop through all legs and collect elevation data:

function calculateElevationProfileData(routeData) {
  const legElevations = [];

  // Collect elevation ranges from each leg
  routeData.properties.legs.forEach(leg => {
    if (leg.elevation_range) {
      legElevations.push(leg.elevation_range);
    } else {
      legElevations.push([]);
    }
  });

  let labels = [];  // Distances
  let data = [];    // Elevations

  // Combine data from all legs
  legElevations.forEach((legElevation, index) => {
    // Calculate cumulative distance across legs
    let previousLegsDistance = 0;
    for (let i = 0; i <= index - 1; i++) {
      previousLegsDistance += legElevations[i][legElevations[i].length - 1][0];
    }

    // Add distances (x-axis)
    labels.push(...legElevation.map(point => point[0] + previousLegsDistance));

    // Add elevations (y-axis)
    data.push(...legElevation.map(point => point[1]));
  });

  return { labels, data };
}
Enter fullscreen mode Exit fullscreen mode

For multi-leg routes (multiple waypoints), we add the distance from previous legs to maintain continuity. This ensures the x-axis shows the total distance traveled, not just the distance within each leg.

💡 Why track cumulative distance?

Without cumulative distance, a multi-leg route would show disconnected segments on the chart. By adding the previous legs' distances, the elevation profile becomes one continuous line from start to finish.


Step 3: Optimize the Data

Long routes can have thousands of elevation points. When we first built the elevation chart, we passed all the data directly to Chart.js, and the browser froze for several seconds on a 500 km route. The lesson: always optimize large datasets before rendering.

The solution is to keep only points that represent significant changes:

// 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]);
  }
});
Enter fullscreen mode Exit fullscreen mode

This keeps:

  • First and last points (always)
  • Points with significant distance gaps (>5m)
  • Points with elevation changes (>10m)

A 500km route might reduce from 10,000 points to 800 points, making the chart render instantly.

💡 Tuning the thresholds

The minDist and minHeight values (5m and 10m in our example) work well for most routes. For mountainous terrain, you might increase minHeight to 20-30m to avoid a jagged chart. For flat routes, lower values preserve more detail. Experiment based on your use case.


Step 4: Render with Chart.js

Create a line chart with the elevation data:

const ctx = document.getElementById("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,
  options: {
    animation: false,
    maintainAspectRatio: false,
    scales: {
      x: {
        type: 'linear',
        title: {
          display: true,
          text: 'Distance (m)'
        }
      },
      y: {
        type: 'linear',
        beginAtZero: true,
        title: {
          display: true,
          text: 'Elevation (m)'
        }
      }
    },
    plugins: {
      legend: {
        display: false
      },
      tooltip: {
        displayColors: false,
        callbacks: {
          title: (tooltipItems) => {
            return `Distance: ${tooltipItems[0].label}m`;
          },
          label: (tooltipItem) => {
            return `Elevation: ${tooltipItem.raw}m`;
          }
        }
      }
    }
  }
};

const chartInstance = new Chart(ctx, config);
Enter fullscreen mode Exit fullscreen mode

Elevation profile chart showing a line graph with blue gradient fill, x-axis labeled distance in meters, y-axis labeled elevation in meters

The elevation profile shows hills and valleys along the route. The filled area makes changes easier to see.

â„šī¸ Chart.js configuration choices

We disable animation (animation: false) because the chart often updates when users select different routes. Animations in this context feel sluggish rather than smooth. We also hide the legend since there's only one dataset. These are UX decisions, not technical requirements.


Step 5: Explore the Demo

The live CodePen demo shows:

  1. Fetching a route with elevation data
  2. Processing and optimizing elevation points
  3. Rendering with Chart.js
  4. The data structure from the API

Experiment with the code

Open the demo in CodePen and try modifying the JavaScript:

  • Change waypoints to a mountain route for dramatic elevation changes
  • Adjust minDist and minHeight thresholds to see optimization effects
  • Try different chart colors and fill styles

Use cases

Scenario Why Elevation Matters
Hiking routes Estimate difficulty and plan breaks
Cycling routes Account for climbs in time estimates
Running routes Calculate calorie burn more accurately
Delivery planning Estimate fuel consumption for trucks
Real estate Show terrain profiles for property access


Summary

Elevation profiles add valuable context to route directions. They help users understand what they're getting into before they start, whether it's a gentle stroll or a challenging climb.

In this tutorial, you learned how to:

  1. Request elevation data from the Routing API using the details=elevation parameter
  2. Extract elevation points from multiple route legs with cumulative distance tracking
  3. Optimize the dataset to avoid performance issues with large routes
  4. Render an elevation profile with Chart.js using appropriate configuration

This is the fourth tutorial in our series on building printable route directions. The series covers:

In the next tutorial, we'll bring all these pieces together into a complete working demo with an interactive map, printable output, and all the features combined.

The full working example is also available on GitHub:
👉 Printable route directions example

Useful links:


FAQ

Q: Which travel modes support elevation data?

A: Elevation is available for walk, hike, bicycle, mountain_bike, road_bike, and some motorized modes. Check the Routing API documentation for the complete list.

Q: How do I highlight steep sections on the chart?

A: Calculate the grade (rise/run) between points and use Chart.js segment styling to color steep sections differently.

Q: Can I export the chart as an image?

A: Yes. Use Chart.js's built-in toBase64Image() method:

const imageUrl = chart.toBase64Image();
// Use in <img> tag or download
Enter fullscreen mode Exit fullscreen mode

Q: Why optimize the data?

A: A 500km route can have 10,000+ elevation points. Rendering that many points slows down the chart. Optimization reduces to ~800 points while keeping the visual profile accurate.

Q: Can I add markers for waypoints on the elevation chart?

A: Yes. Calculate which distance values correspond to waypoints and add them as a separate dataset with pointStyle: 'triangle'.


What's Next?

In the final part of this series, we'll combine all these techniques into a complete working demo. You'll see how the routing API, static maps, step previews, and elevation charts work together in a single application with print functionality.

👉 Part 5: Printable Route Directions - Complete Demo Walkthrough


Try It Now

👉 Open the Live Demo

Please sign up at geoapify.com and generate your own API key to start adding elevation profiles to your routing applications.

Top comments (0)