Telling stories and insights with interactive maps
About this Article
This article is about a story map I produced:
…and this article is the story of how I built it.
Introduction
A cartographer’s products are maps. We work with shapefiles (which is the legacy of spatial data storage), geodatabases, geojsons, and many form of spatial data and use GIS to produce maps out of them. These maps, however, are often JPEGs, PNG, or PDF documents which are not interactive. For most cases, yes it works, but I like telling and being told stories where I don’t have to do much of the work. With less effort, how can I get told stories?
Then I encountered Cuthbert Chow‘s article about D3.js. D3.js is a javascript library to manipulate elements and svg in the DOM (Document Object Model). People use D3.js to present data, in which Chow has done magnificant demonstration, extending Jim Vallandingham’s article. Look at his following article.
How I Created an Interactive, Scrolling Visualisation with D3.js, and how you can too
And then it came down to me: we can make this with maps too! The idea of scrolling and having the data being presented interactively is possible using javascript. This article tells the story of how I did it.
You can find the live demo here:
Stacks
All data and map resources are open sourced (openstreetmap). The following are the stacks I’ve used
- jquery and d3.js : document manipulation
- leaflet.js : interactive web-map
Generic Idea
I write about spatial Data Science, and I wrote an article about presenting spatial data in the following article. This article is the demonstration.
Essentially the idea of how it works is:
- Using Python to acquire and analyze spatial data.
- Using HTML to lay out the documents, CSS to make things beautiful; presenting the content.
- Using Javacript to make things interactive. In this case, change the maps when we scroll.
Additionally (not in this article/project), we can do something regarding the data sources:
- Storing data in postgresql + postgis database server.
- Apache Airflow to orchestrate/automatically acquire data and populate the database server.
- Using geoserver as a backend map server.
Perhaps for future projects.
How it Works
I won’t cover the details, or this article will become very long. What I will explain is the high level idea for developers to… develop.
Most of the code comes from Jim’s article about JavaScript scroller. The demo based on this code is available here.
But in this article I am discussing my demo. I will break it down based on these components:
- Layout
- Sections and Scrolling
- Data Storage
The Layout – Fixing the Map
The layout is based on Bootstrap 5 css module. This is a very common module that beautifies HTML quickly. It provides the essential minimal UI components.
Specifically, I made 2 columns, as coloured by the following image. Grey is the body’s background, blue is the margin, and white/light is the column’s background.
<div class="container">
<div class="row">
<div class="col-4">
the first column, text contents / stories
</div>
<div class="col-8">
the second wider column where the map is going to live
</div>
</div>
</div>
I want the scroll to be interactive to the story/text, but not to the map. This means that the map needs to be fixed regardless of how much we scroll. This is where css comes in. Everything that ignores scroll requires the position to be fixed, and so I created the stay
css class.
.stay {
position: fixed;
height: 100%;
width: 100%
}
and stick it in the div
where the map will live.
<div class="col-8">
<!-- the second wider column where the map is going to live. -->
<div class="stay" id="mapcontainer">
<div id="map" ></div>
</div>
</div>
This way, the map div
will be fixed.
Maps and Javascript
as the map div
is created, we can now import leaflet
javascript library to make our maps. Leaflet
provides the map interactivity tools that I need. It is a brilliant package; so simple but it works!
var map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
L.marker([51.5, -0.09]).addTo(map)
.bindPopup('A pretty CSS3 popup.<br> Easily customizable.')
.openPopup();
Sections and Scrolling
Just like Chow’s and Jim’s structure, the Arriving London page also consists of sections with id step
.
<div class="col-4 full ">
<section class="step ">
my first section
</section>
<section class="step ">
the second section
</section>
<section class="step ">
and so on
</section>
<section class="step ">
...
</section>
</div>
Scroll Interactive
After the steps are defined, the document needs to track the scrolling activity from the user. The following code calls the trackPosition
function every time the user scrolls.
d3.select(window)
.on("scroll.scroller", trackPosition);
The trackPosition
is as the following:
// sectionsArray are the div step (s) from the HTML that we previously defined
let activeSection;
let previousSection;
const trackPosition = ()=>{
let pos = window.pageYOffset - 140;
let sectionIndex = d3.bisect(sectionPositions, pos);
sectionIndex = Math.min(sections.size() - 1, sectionIndex);
activeIndex = sectionIndex
if (sectionsArray[sectionIndex] !== activeSection){
previousSection = activeSection
activeSection = sectionsArray[sectionIndex]
d3.select(`#${activeSection}`)
.transition()
.style("opacity", "1");
d3.select(`#${previousSection}`)
.transition()
.style('opacity', '0.2')
;
positionMap(sectionIndex)
}
}
Please notice the positionMap
function! This function is what makes the map changes.
// positionMap: changes the map based on the active step
const positionMap = (sectionIndex) =>{
if (sectionIndex === 0){
map.flyTo([51.505404,-0.118658], 9,) // zoom in to coords
airportLayer.addTo(map) // leaflet layer
elizaebthLine_st.remove() // leaflet layer
popupairport() // popup the map
attractions.remove() // leaflet layer
}
if (sectionIndex === 1){
map.flyTo([51.509687,-0.115464], 13,) // zoom in to coords
attractions.addTo(map) // leaflet layer
attractions.eachLayer((l)=>{l.openTooltip()}) // open the tooltips
// add another if and manually code the interactivity. Read the leaflet documentation.
}
Data Storage
In a schema-less data structure, which is basically an array containing JSON objects, we can add arbitrary properties. I demonstrate this in the data structure article.
We can define a spatial data as minimal as this:
// minimum spatial data
const berlin = {
"city": "berlin",
"country": "germany",
"location" : {
"type" : "Point",
"coordinates": [52.507650, 13.410298]
}
}
But I confine to the GeoJSON spatial data specification, so the spatial data looks like this:
const attractions_geojson = {
"type": "FeatureCollection",
"features": [
{
// minimum spatial data but with geojson spec
"type": "Feature",
"properties": {
"name": "big ben"
},
"geometry": {
"coordinates": [
-0.12466064329075266,
51.50067738052945
],
"type": "Point"
}
},
{
// minimum spatial data but with geojson spec
"type": "Feature",
"properties": {
"name": "leicester square"
},
"geometry": {
"coordinates": [
-0.13047682089788282,
51.51079955591317
],
"type": "Point"
}
}
]
}
The attractions_geojson.features
containing our layers, in the code above, consists of 2 layers: Big Ben, and Leicester Square. The geojson.features
is a list of all of the minimal spatial data.
You can view the spatial data in the variable.js
when you load the demo link in your browser.
Conclusion
Using Javascript and HTML, we can make our data interactive. As a cartographer this means that I can make my maps interactive and responsive! The tricky bit is to store the spatial data as a javascript file instead of a conventional shapefile and geojson object.