Interactive maps are a powerful tool for data visualization and user interfaces. Providing rich content based on geographic location is the cornerstone of many apps such as Yelp, Zillow, and Road Trippers. In this post we are going to create an interactive map of campgrounds using the Google Maps API, JavaScript, and Python.

The codepen below shows what we’ll be building:

See the Pen Finished Map by Sev (@sevleonard) on CodePen.



What We Will Achieve Here

Build a Google map with JavaScript. Convert geographic campground data from a CSV to GeoJSON so Google Maps can read it. Load the GeoJSON into Google Maps and set up click events and interactivity. Go camping at Crater Lake (optional).

Designing with Maps

A good map-based application, or really, any good application, begins with thinking about what functionality you want to provide and how users will interact with it. Here are some questions to consider while you’re specing things out.

What geographic region do you want to map? What kind of map makes the most sense for your application? Topographic, traffic, congressional districts? What information do you have to add to the map? How will end-users interact with the map?

For this example, we want to build a map to help people find a place to camp near Crater Lake, Oregon. Crater Lake is a popular tourist destination and the deepest lake in the United States, though folks from Lake Tahoe may disagree. Disagreements aside, we know we want a map of the Crater Lake area and we want to display information about campgrounds.

Regarding the type of map to use, let’s think about what would be important to a person looking for a place to camp. Certainly a camper would want to know how they can get to Crater Lake from a campground, so we definitely should pick a map that shows roads. Perhaps we read a study of outdoor recreation and found that most campers prefer to camp in shaded areas. With satellite imagery campers could identify shaded campgrounds by looking at the tree cover, so that seems like a good bit of information to include in our map.

Using information from the National Forest Service, we have a dataset of eleven campgrounds closest to Crater Lake. Each data point includes the campground name and location with some additional features on campground amenities: showers and toilet type – vault or flush. We’ll display the data using map markers for each campground, displaying the amenities if the user clicks the marker to learn more, like this:

Now that we have an initial design, let’s get into the details of making it happen.

Setting up a map element

We’ll setup our campground finder as a web app that runs in the browser. We need a reference to the Google Maps API and a div element where the map will be displayed.

<script src="https://maps.googleapis.com/maps/api/js"></script> <div id="map"></div>

For this example we are not specifying an API key, so you will see a warning “NoApiKeys” when running the code in codepen. If you are using the Google Maps API for your web application you should get an API key and note the usage limits. As long as you stay under 1,000 map loads / hr you should be good with the free plan.

One last thing before we move on – we need to set the size of the map div. If you forget to do this, you’ll find yourself staring at a blank page. Not super helpful for finding a place to camp.

#map { width: 600px; height: 500px; }

Customizing the map

At this point our app isn’t very exciting. Just a blank HTML page. Now we’ll setup and display the map, recalling our design specs.

Map types – We decided we wanted to show roads and satellite images for our campground map. Conveniently this sort of map is one of the basic map types, hybrid.

Zoom Level – The initial level of detail is determined by setting the zoom level. A zoom level of 10 will give us enough detail to show all the campgrounds near Crater Lakes and the major roadways.

Center – as you would likely expect, this is the latitude, longitude where you’d like to center the map. Our map is centered at Crater Lake.

We’ll encapsulate these parameters in the map_options object:

map_options = { zoom: 10, mapTypeId: google.maps.MapTypeId.HYBRID, center: {lat: 42.9446, lng: -122.1090} }

Initializing the Map

Using the Map constructor, we pass the map_options and the map div to create a new object “map”. We will refer to the map object later when we are ready to add the campgrounds.

map_document = document.getElementById('map') map = new google.maps.Map(map_document,map_options);

Let’s set the map to draw when the page is loaded. To do this, we’ll add a call to addDomListener. The initMap function containing our map setup code will be called when the window is loaded.

function initMap() { map_options = { zoom: 10, mapTypeId: google.maps.MapTypeId.HYBRID, center: {lat: 42.9446, lng: -122.1090} } map_document = document.getElementById('map') map = new google.maps.Map(map_document,map_options); } google.maps.event.addDomListener(window, 'load', initMap);

Alright! We are now all set to load a map. If you run the codepen you should see this map of Crater Lake and the surrounding area

See the Pen Finished Map by Sev (@sevleonard) on CodePen.

Getting data for our map

Now that we have a map, lets get some data to add to it. Google Maps can read data in a variety of different formats, including GeoJSON, which we will be using in this example. All we need now is a bit of Python code to convert the data to GeoJson. The code in the next section uses Python 3.4.3 and version 0.18.1 of the pandas library. You can follow along using the Jupyter notebook here

Data cleaning

When working with data it’s important to begin by reading it! We need to convert our data from a CSV format to the GeoJSON format that Google Maps can read. Using the Python pandas library, we can read data into a tabular structure called a DataFrame. We can use this object to observe, analyze and manipulate the data. The campground data is in a CSV format, we can use pandas read_csv to read this into a DataFrame.

# import the modules we need to convert the CSV data to GeoJSON import pandas as pd import numpy as np from pandas import json cg_data = pd.read_csv('campgrounds.csv') # inspect the data, take a look at its shape (rows, columns) and the first # few rows of data cg_data.shape >> (11, 6) cg_data.head()

Each row represents a campground, with its name denoted by the facilityname field, a string. The latitude and longitude in numeric format are next, followed by the amenities – flush toilets, showers, and vault toilets. The amenities fields have three possible values: 0,1, and ‘\N’. 0 denotes False, 1 is True, and ‘\N’ indicates the data is not available.

We want to transform this data into valid GeoJSON, for example:

Example GeoJSON:

{ "type": "Feature", "properties": { "title":"Ainsworth State Park", "description": "Flush toilet, shower" } "geometry": { "type": "Point", "coordinates":[-122.048974,45.59844] } }

We’ll use the facility name for the ‘title’ field, and create a string of the amenities for the ‘description’ field. A few lines of Python to change some column names and replace amenity field values should do the trick:

cg_data_clean = cg_data # replace the amenity values with the appropriate strings cg_data_clean = cg_data_clean.replace({'flush': {'1':'Flush toilet', '0':'', '\\N':''}}) cg_data_clean = cg_data_clean.replace({'shower': {'1':'Shower', '0':'', '\\N':''}}) cg_data_clean = cg_data_clean.replace({'vault': {'1':'Vault toilet', '0':'', '\\N':''}}) # rename columns to be consistent with the GeoJSON field names cg_data_clean = cg_data_clean.rename(columns={'facilityname': 'title', 'facilitylatitude':'latitude', 'facilitylongitude':'longitude'})

We can concatenate the amenity fields into a single ‘description’ field using the .join() function inside an apply. The apply function enables you to perform operations over rows (axis=0) or columns (axis=1) of a DataFrame. This leaves us with some extraneous leading and trailing commas for this data set, so we also have a helper function, clean_description, to strip leading and trailing commas.

# create description field cg_data_clean['description'] = cg_data_clean[['flush','shower','vault']].apply(lambda x: ', '.join(x), axis=1) # function to clean the leading / ending commas from the description. This won’t remove commas in between fields, though! def clean_description(description): description = description.strip() while((description.startswith(',') or description.endswith(',')) and len(description) > -1): if description.endswith(',') : description = description[0:len(description)-1] if description.startswith(',') : description = description[1:len(description)] description = description.strip() return description # apply the clean_description function to all rows cg_data_clean['description'] = cg_data_clean.description.apply(lambda x: clean_description(x))

We no longer need the individual amenity columns, having concatenated them into the description column, so we can drop those moving forward. We’re ready to convert the DataFrame to GeoJSON, which we will again call on our new friend apply to help with. Thanks to Geoff Boeing and his post on Exporting Python Data to GeoJSON for some insight here.

Recalling our GeoJSON example above, each campground will be a feature, part of a FeatureCollection object. The approach here is to create a collection object and add features to it as they are processed by the feature_from_row method.

# create the feature collection collection = {'type':'FeatureCollection', 'features':[]} # function to create a feature from each row and add it to the collection def feature_from_row(title, latitude, longitude, description): feature = { 'type': 'Feature', 'properties': { 'title': '', 'description': ''}, 'geometry': { 'type': 'Point', 'coordinates': []} } feature['geometry']['coordinates'] = [longitude, latitude] feature['properties']['title'] = title feature['properties']['description'] = description collection['features'].append(feature) return feature # apply the feature_from_row function to populate the feature collection geojson_series = geojson_df.apply(lambda x: feature_from_row(x['title'],x['latitude'],x['longitude'],x['description']), axis=1)

Looking at the ‘collection’ object we can see how the DataFrame was converted to GeoJSON. Here are the first two features in our FeatureCollection:

Using pandas json module we can write the collection out to a geojson file with the correct formatting. We’ll reference this file to pull in the campground data for the map.

with open('collection.geojson', 'w') as outfile: json.dump(collection, outfile)

Adding GeoJSON data to the map

We have our base map and our campground GeoJSON. We just need a little more JavaScript to pull all of it together. Google Maps has a super convenient loadGeoJson method that we can use to get our points onto the map right away:

geojson_url = 'https://raw.githubusercontent.com/gizm00/blog_code/master/appendto/python_maps/collection.geojson' map.data.loadGeoJson(geojson_url, null, loadMarkers)

Running this in codepen, you should see markers added to the map. Those are the campgrounds! Hover over a campground to see its name given in the ‘title’ field.

The loadMarkers parameter in the loadGeoJson call above is a callback function we will use to add information to the markers. Once the GeoJson is loaded, loadMarkers gets called and the marker objects and pop up windows (info windows) are created. The InfoWindow content field is a string that accepts HTML tags, so you can format and style the pop up as you like. This last codepen includes the code for adding the infoWindow to the markers.

function loadMarkers() { console.log('creating markers') var infoWindow = new google.maps.InfoWindow() map.data.forEach(function(feature) { // geojson format is [longitude, latitude] but google maps marker position attribute // expects [latitude, longitude] var latitude = feature.getGeometry().get().lat() var longitude = feature.getGeometry().get().lng() var titleText = feature.getProperty('title') var descriptionText = feature.getProperty('description') var marker = new google.maps.Marker({ position: {lat: latitude, lng:longitude}, title: titleText, map: map }); var markerInfo = "<div><h3>" + titleText + "</h3>Amenities: " + descriptionText + "</div>" // by default the infoWindow for each marker will stay open unless manually closed // using setContent and opening the window whenever a marker is clicked will // cause the prior infoWindow to close marker.addListener('click', function() { infoWindow.close() infoWindow.setContent(markerInfo) infoWindow.open(map, marker) }); markers.push(marker) }); }

We’re all done! Try it out and see which campground you’d prefer. One last note, we’re keeping an array of the current markers for later use when it will be needed for functions like filtering.

Summary

Congratulations! You now know how to clean and format raw data and use it to create an interactive map. Maybe you can think of some additional information you’d like to add to the map like current weather or cell phone coverage. You can follow the same procedures we used for cleaning the campground data to other information sources, and adding these features to the ‘description’ GeoJSON field to easily include it on your map. Happy mapping!