Get your team started on a custom learning journey today!
Our Boulder, CO-based learning experts are ready to help!
Get your team started on a custom learning journey today!
Our Boulder, CO-based learning experts are ready to help!
Follow us on LinkedIn for our latest data and tips!
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
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.
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!
Customized Technical Learning Solutions to Help Attract and Retain Talented Developers
Let DI help you design solutions to onboard, upskill or reskill your software development organization. Fully customized. 100% guaranteed.
DevelopIntelligence leads technical and software development learning programs for Fortune 500 companies. We provide learning solutions for hundreds of thousands of engineers for over 250 global brands.
“I appreciated the instructor’s technique of writing live code examples rather than using fixed slide decks to present the material.”
VMwareDevelopIntelligence has been in the technical/software development learning and training industry for nearly 20 years. We’ve provided learning solutions to more than 48,000 engineers, across 220 organizations worldwide.
Thank you for everyone who joined us this past year to hear about our proven methods of attracting and retaining tech talent.
© 2013 - 2022 DevelopIntelligence LLC - Privacy Policy
Let's review your current tech training programs and we'll help you baseline your success against some of our big industry partners. In this 30-minute meeting, we'll share our data/insights on what's working and what's not.
Training Journal sat down with our CEO for his thoughts on what’s working, and what’s not working.