Storytelling with Scroll-Driven Maps

As a software engineer who enjoys writing, one thing that really excites me is interactive storytelling. The internet is a powerful medium, with great potential for some unique reading experiences. The New York Times leads the way with their interactive content section. Bret Victor’s Explorable Explanations post also provides some excellent insight into how interactivity can be applied.

When I write, I try to use interactive features to increase reader engagement. Since these features are driven by the story, they provide context that static content alone cannot. Consider writing about traveling to a new place, for example. Words paint the experience, and images are a glimpse of what was seen, but including an interactive map adds a valuable layer of detail. Plus, maps are just plain cool.

This post is a guide to creating a scroll-driven, interactive map to accompany your stories.


Overview

As the reader scrolls through the different sections of the story, the map will update to reflect what is being described.

Our story will be about a road trip between San Francisco, Los Angeles, and San Diego. Our map will draw the path taken on the road trip. We start by splitting the story into sections; scrolling through one will cause the path to update accordingly.

<section id="san-francisco">
The first stop of our road trip was San Francisco. Even though we are very familiar with city, we decided to act as tourists on our visit...
</section>
<section id="los-angeles">
A solid 6 hours of driving later, I knew we reached Los Angeles when I saw two things: traffic, and smog...
</section>
<section id="san-diego">
At least, we reached San Diego, the end of our trip, and one of my favorite places in the world...
</section>

By syncing the story and the path together, the reader gets an integrated sense of the overall journey. This style works especially well for stories about hiking or backpacking, where the path is part of the trip’s DNA.

Creating the map

We’ll use Mapbox.js (built on top of the popular Leaflet library) to create our map. Rendering the map is as simple as referencing an HTML element, choosing a tile set (map style) and setting the center and zoom.

<body>
<div id="map" style="height: 500px; width: 500px"></div>
</body>
# Javascript
L.mapbox.accessToken = 'yourAccessToken';
var map = L.mapbox.map('map', 'mapbox.streets');
var ZOOM_LEVEL = 6;
var center = [35.1738083, -121.61865] // somewhere in California
map.setView(center, ZOOM_LEVEL);
Don’t get too excited — it’s just a screenshot

Play around with different map styles — light is my personal favorite.

I spend an unfortunate amount of time tinkering with coordinate values to get the map centered perfectly. To expedite the process, I add an event handler to log the new center of the map after it has been dragged.

map.on('dragend', function() {
console.log('New center: ', map.getCenter());
});

Next, generate the coordinates that will make up the path.

var SF = L.latLng(37.774929, -122.4194167);
var LA = L.latLng(34.052234, -118.243685);
var SD = L.latLng(32.715738, -117.161084);

For the sake of simplicity, our map’s path will only contain three points. Generating the actual path requires a bit of work, but can still be done relatively quickly. I’ve found these resources are of great value:

The points will be rendered into a polyline, which will initially be empty.

var path = L.polyline([], {'color': '#FF0000');

Adding Interactivity

Since our map is scroll-driven, an onscroll event handler implements the interactivity. It does so in two parts: determining the “active” section, and then rendering the changes linked to that section. In our case, the changes are updates to our path.

Step 1 is done with the help of getBoundingClientRect.

When called on an element, getBoundingClientRect returns a read-only object. These properties of the object (top, bottom, left, right, height, width, x, and y) can be used to determine the current position of the element. Top measures the length (in pixels) from the top-left corner of the window to the top edge of the element in question, left measures the length from the corner to the left edge, etc.

A few helpful rules:

  • If top is greater than the height of the window, the reader has yet to scroll to that element.
  • If bottom is less than zero, then the reader has completely scrolled past the element.
  • Any element with both top and bottom between zero and the window height is completely within the window bounds.

function getActiveSection() {
var sections = $('section');
var windowHeight = window.innerHeight;
var currId = '';
  for (var i = 0; i < sections.length; i++) {
var rect = sections[i].getBoundingClientRect();
    // Element's bottom edge has reached the bottom of the screen
if ((rect.bottom + BUFFER) < windowHeight) {
currId = sections[i].id;
}
}
return currId;
}

Rendering Changes

The second step involves linking each section with the changes that need to be made to the path. A straightforward approach is to create a Javascript object with keys equal to the id’s of the sections. The object’s values are the coordinates that will be added to the path.

// SF, LA, and SD are coordinates defined above
idToCoordinates = {
'san-francisco': SF,
'los-angeles': LA,
'san-diego': SD
}
function drawPath() {
var activeId = getActiveId();
var coordinates = idToCoordinates[activeId];
path.addLatLng(coordinates);
}

Set the drawPath function to be the onscroll event handler of whatever HTML element contains the text of the story, and…that’s it!


Interactivity has so much potential to influence the way we engage with content. I’m looking forward to working on stories with new interactive wrinkles. More generally, I’m excited to see what the internet is capable of creating.

Thanks for reading. I hope you found this post informative — or even better, I hope you use this post as a springboard to create your own maps and tell your own stories.

Credits

The idea for the map was largely inspired by this example from the Mapbox’s website:

https://www.mapbox.com/mapbox.js/example/v1.0.0/scroll-driven-navigation/