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:
- Routing API - get elevation data with routes
- Chart.js - render the elevation chart
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
- Request Elevation Data
- Extract Elevation Points
- Optimize the Data
- Render with Chart.js
- Explore the Demo
- Summary
- 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];
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]
]
}]
}
}]
}
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 };
}
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]);
}
});
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
minDistandminHeightvalues (5m and 10m in our example) work well for most routes. For mountainous terrain, you might increaseminHeightto 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);
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:
- Fetching a route with elevation data
- Processing and optimizing elevation points
- Rendering with Chart.js
- 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
minDistandminHeightthresholds 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:
-
Request elevation data from the Routing API using the
details=elevationparameter - Extract elevation points from multiple route legs with cumulative distance tracking
- Optimize the dataset to avoid performance issues with large routes
- 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:
- Part 1: Requesting routes and turn-by-turn directions
- Part 2: Generating route overview images
- Part 3: Creating step preview maps
- Part 4: Adding elevation profiles (this tutorial)
- Part 5: Complete demo walkthrough
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
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)