webMapWorkshop-logo-01

Web Cartography and Customization

Prerequisite: Make a Basic Web Map and JavaScript: A Mapper's Introduction


Download the Workshop Materials. This is Session 5.


It's time to look at some cartographic components of our map, including map elements, symbolization, and customization! When creating a web map, one of the key components is styling your elements to provide proper symbolization for your data. This increases legibility for users, allows for conveyal of data and information, and can give your map an appealing, custom design. Elements that can be custom designed include, but are not limited to, styling data (points, lines, and polygons), basemaps (tile layers), user interface (the components of the map that allow for user interaction), and legends and supplemental information (such as supplemental prose and titles).

City of Cambridge Housing Inspector Data

Up to now and in the previous sessions, we have been looking at coffee shops around Cambridge. This week, let's change things up a bit and try a different dataset! The City of Cambridge runs an open data site through Socrata that contains loads of data on hundreds of various civic topics, ranging from Potholes, Bicycle Accidents, Census Demographics, and even Health Inspector data. I downloaded a dataset for us that contains Housing Inspector rodent violations (ew!!) from March 2014 through September 2014 that we can map and explore.

To get a visual, this is what we are going to make today.

To get started, setup your development environment in a easy to access location on your machine. Download the package containing the materials for this week, unzip the contents, and start up your localhost server in that location.

Start your Localhost Server
Fire up a web page for your map on your local machine to prepare to make some changes. To do this, serve your page on a localhost server on your machine. Open a command line (Command Prompt (Windows) or Terminal (Mac)), use cd to change the directory to the one where you placed the materials package you downloaded.

Once in that location start your basic Python SimpleHTTPServer by entering python -m SimpleHTTPServer and pressing return.

Then open a brower tab to http://localhost:8000/map1.html. This will take us to Map 1.

For more instructions on this, read the tutorial on localhost servers here.

0. Set up our Map and Add Data

Use map1.html in the materials for the tutorial
With your localhost running, open up a copy in your web browser and your text editor to prepare for editing. When open, you should see a basic map showing the extent of the City of Cambridge. View this Example View the map1.html document in Sublime Text (or your text editor of choice). Here you will see our HTML, with some CSS styling at the top, a couple of div page elements for our map components, linked scripts, and custom scripts. The page elements are as follows:
  • wrapper: the main container for our map in the body. Our whole map and interface will fall inside the wrapper element.
  • header: an element in which we can put the title of our map.
  • map: the element our map will be attached to.
  • controls: the element in which we can put any buttons or controls
  • credits: an element inside controls where we can put our contact and copyright information
No buttons or controls are added to our map at the moment, but you can add interactivity using some of the methods and tricks we learned in the session from last week on JavaScript.

The Script

Within the script tags, I have added the map object and tile layer for us to use. You've seen the script before. This script creates our map object and adds a basemap.
The tile layer I am using comes from the Metropolitan Area Planning Council, using data from MassGIS. In addition to worldwide base layers you find from major organizations, many local municipalities and regions around the globe maintain tile layers that can be accessed through GIS software and mapping libraries. Google search the area in which you are working to see if they maintain base maps, most often you will find Web Map Services (WMS), that can be loaded into your map as a base using the Leaflet WMS loader object, L.tileLayer.WMS.

Add the Rodent Violation Data

Next, we want to add the dataset to the map. Note that I have included the jQuery library in our document so we can use an Asynchronous call to get external data (AJAX) and the $.getJSON method to add our dataset from an external location. In the data folder of the downloaded materials, you will find our dataset, rodent_violations.geojson. At the end of our script, within the script tags, enter the following code to add the rodent violations dataset and bind a popup.
// Add Rodent Violation GeoJSON Data
// Null variable that will hold rodent violation data
var violationPoints = null;

// Get GeoJSON and put on it on the map when it loads
$.getJSON("data/rodent_violations.geojson",function(data){
    // set violationPoints to the dataset, and add the rodent violation GeoJSON layer to the map
    violationPoints = L.geoJson(data,{
		onEachFeature: function (feature, layer) {
			layer.bindPopup(feature.properties.address);
		}
	}).addTo(map);
});
The violationPoints variable will hold the contents of our GeoJSON so we can refer to it easily. Save and refresh your map. You should see the points populate. That is a lot of rodents! View Example on its own (take a look at Page Source)

1. Custom Point Markers

Our point markers showing the Housing Inspector Rodent Violations are the default blue Leaflet map pin. While these markers are fine, if you are showing multiple properties (i.e. open cases versus closed cases) or want to create unique symbols, you can set your point symbols to be represented by an icon of your choosing. The steps towards doing this are quite easy, and you can use the Leaflet icon class to set up your parameters. You have two choices for your custom icons. First, you can use existing icons or grab a library of pre made icons. Here are two nice leaflet plugins: Second, you can make your own icons by using an existing image or creating one using graphics software (i.e. Illustrator, Photoshop, Inkscape, or GIMP). If your graphic is saved as an image (the most space efficient images for the web are usually in png or jpg format), and upload it to your server for use as an icon. If you want a higher level of customization, or the icons found in Font Awesome or Maki do not work for you, create your own!

a. Creating a Custom Marker

In the downloaded data package for this week, you will see a folder named images. It contains a handful of images that can be used as markers (note, I included a bunch of rodent icons, and then a few coffee cup icons in the folder if you want to customize your coffee shop map from last week!). To get started, since we are working with rodent incidents, lets change our default blue map pin to be an icon of a small rat: (rodent.png).

i. Use the L.icon object and create a holder for our custom rodent icon

The first task is to create an object that will hold our custom icon. Enter the following code to created our custom rodent icon, within the script tags. Note it will be a global variable, meaning we can use the variable at any location in our script.
// Create Custom Icons Here
// Rodent Icon
var rodentIcon = L.icon({
	iconUrl: 'images/rodent.png',
	shadowUrl: 'images/rodent_shadow.png',
	iconSize: [36,36],
	shadowSize: [36,36],
	iconAnchor: [18,18],
	shadowAnchor: [18,18],
	popupAnchor: [0,-6]
});
This used the Leaflet Icon class (L.icon) to set up the icon. L.icon require a path to your icon, then takes a handful of other options that allow you a high level of control. Creating this icon as an object and saving it as rodentIcon will allow us to set the display of feature on the to this icon. Let's talk a bit more about these options.
  • iconUrl: contains the path to your icon
  • shadowUrl: contains the path to the icon shadow to give a 3D feel to your map
  • iconSize: sets the size of your icon in pixels. Best to work at size, okay to scale down, never scale up
  • shadowSize: sets icon shadow image size
  • iconAnchor: sets anchor point (where the icon is located in respect to the feature latitude and longitude
  • shadowAnchor: sets anchor point of shadow
  • popupAnchor: sets anchor point of bottom of popup

Icon Anchors

The anchors are where your icon touches the map, and are initially confusing, so is important to clarify how they work. The iconAnchor coordinates are set from a 0,0 point at the upper left corner of the icon. If our image is 36px by 36px, to get the anchor to set on the center (i.e. to make the center our exact latitude and longitute) set the iconAnchor to 18,18. If our icon were a pushpin, we could change the anchor so that the end of the pushpin would be located at the point. The popupAnchor sets where the popup points to. The popupAnchor is set relative to the iconAnchor, meaning the 0,0 point for the popup is actually at the set iconAnchor point. Diagramatically, it looks something like the following. icon_coordinates_2 When complete, the variable rodentIcon now contains our rodent image.

ii. Use point to layer option of L.geoJson to set the icon

With our icon loaded into our document, we need to replace the default icons created when we add the GeoJSON. This is a process of setting the icon option to rodentIcon for each marker when it is added to our map. To set the icon for a GeoJSON, we need to create a layer from the GeoJSON (we can style it if it is a layer) by using the pointToLayer option of L.geoJson. Our GeoJSON is added to our map using jQuery with the following block of code. We broke this down in depth in last week's session, JavaScript: A Mapper's Introduction. Locate the following block of code in your map, it adds the GeoJSON to the map and uses the onEachFeature option to bind a popup to each feature when it is created.
// Get GeoJSON and put on it on the map when it loads
$.getJSON("data/rodent_violations.geojson",function(data){
    // set violationPoints to the dataset, and add the rodent violation GeoJSON layer to the map
    violationPoints = L.geoJson(data,{
        onEachFeature: function (feature, layer) {
            layer.bindPopup(feature.properties.address);
        }
    }).addTo(map);
});
Options available for L.geoJson include:
  • pointToLayer: Function that will be used for creating layers for GeoJSON points (if not specified, simple markers will be created).
  • style: Function that will be used to get style options for vector layers created for GeoJSON features.
  • onEachFeature: Function that will be called on each created feature layer. Useful for attaching events and popups to features.
  • filter: Function that will be used to decide whether to show a feature or not.
  • coordsToLatLng: Function that will be used for converting GeoJSON coordinates to LatLng points (if not specified, coords will be assumed to be WGS84 — standard [longitude, latitude] values in degrees).
We are using onEachFeature to set the popup, but you can see that in order to set a icon, we need to use pointToLayer. In pseudo-code, pointToLayer runs a function when the GeoJSON is loaded that takes a feature and latitude and longitude and creates a marker at that latitude and longitude. Marker has an option called icon that you set to be our rodentIcon variable. Once set, return the marker. This will replace the default blue map pin with our rodentIcon. The code looks like the following. Replace your L.geoJson call with this. Note the addition of the pointToLayer option.
// Get GeoJSON and put on it on the map when it loads
$.getJSON("data/rodent_violations.geojson",function(data){
    // set violationPoints to the dataset, and add the rodent violation GeoJSON layer to the map
    violationPoints = L.geoJson(data, {
        onEachFeature: function (feature, layer) {
            layer.bindPopup(feature.properties.address);
	}, pointToLayer: function (feature, latlng) {
            var marker = L.marker(latlng,{icon: rodentIcon});
            return marker;
        }
    }).addTo(map);
});
Click save and refresh your map in your browser. Check out our map. We have changed the icon to a rodent! View Example on its own (take a look at Page Source)

c. Create an Icon Class and Change Icons for Open and Closed Cases

If we want to symbolize the different statuses of rodent incidents with different icons, we can create an icon class. One of the goals of programming is to never repeat code, and creating a class for the icons will allow us specify anchors and sizes once, and we can change out icons. For example, say we want to set all violations with a status of cited to be shown as a red rodent, and all cases that have been corrected to a gray rodent. In our GeoJSON, the status property contains this information for each feature and can be accessed by feature.properties.status. Open cases are equal to "Cited" and closed cases are equal to "Corrected". We probably don't want to enter all of the specifics on sizes and anchors again, they will remain the same. We can create a class of icons that will allow us to only specify our icon image, keep all other options unchanged.

Icon Class

// Create Custom Icons Here
// Icon Class
var RodentIcon = L.Icon.extend({
    options:{
        shadowUrl: 'images/rodent_shadow.png',
        iconSize: [36,36],
        shadowSize: [36,36],
        iconAnchor: [18,18],
        shadowAnchor: [18,18],
        popupAnchor: [0,-6]
    }
});

// Create specific icons
var citedIcon = new RodentIcon({iconUrl: 'images/rodent_open.png'});
var correctedIcon = new RodentIcon({iconUrl: 'images/rodent.png'});
The class option is built on top of, or extends, the L.icon object. More reading can be found on defining icon classes in the Leaflet documentation. An important note, classes begin with a capital letter (Icon vs icon).

Use a Conditional in our get GeoJSON to determine status

We have in our document two icons, one for corrected cases (correctedIcon) and one for open cases (citedIcon). When the GeoJSON is added to the map, we need to check the features when we apply the custom icon to see what the value of feature.property.status is. If it is equal to "Cited", we want the icon to be set to "citedIcon", if it is equal to "Corrected", we want the icon set to "correctedIcon". Last week, we learned about conditionals, specifically If.. Else statements. To accomplish this, we can put a conditional in our call to the GeoJSON that checks to see if a case status is equal to "Cited" and then sets an icon, and if it is not, will run the else statement, setting the icon equal to "correctedIcon". The code will look like this:
if (feature.properties.status == "Cited"){
	var marker = L.marker(latlng,{icon: citedIcon});
} else {
	var marker = L.marker(latlng,{icon: correctedIcon});
};
return marker;
Note there are two equal signs (==), this is because JavaScript is very particular about operators. To read more, check out this documentation from w3schools. Modify the code within the L.geoJson pointToLayer option, where we set the style previously, to be the following, including the conditional statement to check the status.
// Get GeoJSON and put on it on the map when it loads
$.getJSON("data/rodent_violations.js",function(data){
    // set violationPoints to the dataset, and add the rodent violation GeoJSON layer to the map
    violationPoints = L.geoJson(data,{
        onEachFeature: function (feature, layer) {
            layer.bindPopup(feature.properties.address);
        }, pointToLayer: function (feature, latlng) {
            if (feature.properties.status == "Cited"){
                var marker = L.marker(latlng,{icon: citedIcon});
            } else {
                var marker = L.marker(latlng,{icon: correctedIcon});
            };
        return marker;
        }
    }).addTo(map);
});
Save and refresh your map. Red rodents signify open cases, gray signify corrected cases. View Example on its own (take a look at Page Source)

3. Polygon Data and Symbolization

There are quite a few rodent points on our map! Let's look at the neighborhoods in which the violations occurred. In our data folder, you'll see a second dataset, cambridge_neighborhoods.geojson. The neighborhoods GeoJSON file contains numbers for the number of rodent incidents per square mile per neighborhood, calculated in QGIS, and some additional information about each neighborhood. Some of the additional information includes population and land area by square mile. To add the data to the map, create another L.geoJson object using the jQuery $.getJSON method. Enter the following lines of code at the end of your script, staying within the script tags.
// Null variable that will hold neighborhoods layer
var neighborhoodsLayer = null;

// Add Neighborhood Polygons
$.getJSON("data/cambridge_neighborhoods.geojson",function(data){
    neighborhoodsLayer = L.geoJson(data).addTo(map);
});
Save and refresh your map. Cambridge neighborhoods will be displayed on the map, symbolized in a default blue. View Example on its own (take a look at Page Source) Let's do something about that default blue and thematically style our data to these polygons useful by turning them into a choropleth layer. The neighborhoods GeoJSON file contains numbers for the number of rodent incidents per square mile in each neighborhood, calculated in QGIS. Symbolize the neighborhoods on the map by rodent incident density. This is a three step process. L.geoJson contains a style option that contains styling properties.

a. Set up function for Color Ramp

The first step is to set up a function for our color ramp. This is where we set up our classification breaks and color scheme. Setting up a classification scheme can be tricky. The easiest way to set up a standard classification scheme is to use QGIS, select something like Jenk's Natural Breaks, and copy the break numbers. To set up the function for our classes and colors, create a function, call it setColor, and then return your classes. Enter the following function in your script tags.
// Set function for color ramp
function setColor(density){
	return density > 85 ? '#a50f15' :
	       density > 15 ? '#de2d26' :
	       density > 8 ? '#fb6a4a' :
	       density > 4 ? '#fcae91' :
	                     '#fee5d9';
};

b. Set style GeoJSON style function

Next, set up a function that will set the properties for the L.geoJson style option. Call this function style, pass it a GeoJson feature when invoked. Set the fillColor property to be equal to the setColor function, passing it the rodentDensity property from the GeoJSON. Enter the following code into your script.
// Set style function that sets fill color property equal to rodent density
function style(feature) {
    return {
        fillColor: setColor(feature.properties.rodentDensity),
        fillOpacity: 0.7,
        weight: 2,
        opacity: 1,
        color: '#ffffff',
        dashArray: '3'
    };
}
fillColor and fillOpacity set properties for the fill, and weight, opacity, color, and dashArray set properties for the border.

c. Set style option for the GeoJSON

The final step is to set the style option for the neighborhoods layer GeoJSON. Find the Add Neighborhoods Polygons block of code and modify it to include the style option. Set style equal to style to run the style function when the GeoJSON is create. The modified Add Neighborhoods Polygons code is the following.
// Add Neighborhood Polygons
$.getJSON("data/cambridge_neighborhoods.geojson",function(data){
	neighborhoodsLayer = L.geoJson(data, {style: style}).addTo(map);
});
Save and refresh your map. Load your page to see our styled polygons! View Example on its own (take a look at Page Source)

4. Map Elements

Our map is looking good, but we need a legend to make sense of our data. We could enable popups for each of the neighborhoods, but with popups already enabled on our points, it might be overwhelming to the user. Instead, lets add a legend to our map that contains the information the reader will need to know about the data, colors, and classifications, and then add a scale bar to the corner of the map. The main Leaflet object we will use in this section is the Control object, or L.control. It allows for adding various elements to your map.

a. Add a Legend

Adding a legend is easy, but requires quite a bit of code. The workflow to create a legend involves creating a Leaflet control, setting the control to populate with HTML that represents the legend components, and styling the HTML with CSS so they appear properly on our screen. I'm going to throw a bit more code at you this time, and we will walk through what it is doing. Enter the following block of code to your script (stay in those script tags!).
// Create Leaflet Control Object for Legend
var legend = L.control({position: 'topright'});

// Function that runs when legend is added to map
legend.onAdd = function (map) {

	// Create Div Element and Populate it with HTML
	var div = L.DomUtil.create('div', 'legend');		    
	div.innerHTML += 'Violation Density
'; div.innerHTML += 'by Neighborhood
'; div.innerHTML += 'Violations/Sq. Mi.
'; div.innerHTML += '

85+

'; div.innerHTML += '

15-85

'; div.innerHTML += '

8-15

'; div.innerHTML += '

4-8

'; div.innerHTML += '

0-4

'; div.innerHTML += '
Individual Violations'; div.innerHTML += '
Cited'; div.innerHTML += '
Corrected'; // Return the Legend div containing the HTML content return div; }; // Add Legend to Map legend.addTo(map);
So, what did we do here? First, we created an instance of a Leaflet Control object, calling it Legend, and used the position option to tell it to locate in the top right of our map. Next, we used the onAdd method of the control to run a function when the legend is added. That function creates a new div in the DOM, giving it a class of legend. This will let us use CSS to style everything using the legend tag. In the newly created div, we are going to populate it with HTML by using a built-in JavaScript DOM method called innerHTML. Using innerHTML allows us to change the content of the HTML and add to the legend div. Using the plus-equal += instead of equal = is the syntax for append. Everytime this is used, code following is appended to existing code. In this, we write the HTML we want to use in our legend. Note, the i tag represents our legend icons. Within the HTML, fill in the colors and ranges so that they match our data classfication. After the HTML is appended, return the div element. Lastly, add the legend to the map.

Use CSS to Style

If we save and refresh, the items you see won't make much sense, we need to use CSS to give them placement and organization on the page. The following CSS code will style our elements. Enter it between the style tags in the head of your HTML document. Like above, we'll then walk through what it does.
.legend {
	line-height: 18px;
	color: #333333;
	font-family: 'Open Sans', Helvetica, sans-serif;
	padding: 6px 8px;
	background: white;
	background: rgba(255,255,255,0.8);
	box-shadow: 0 0 15px rgba(0,0,0,0.2);
	border-radius: 5px;
}

.legend i {
	width: 18px;
	height: 18px;
	float: left;
	margin-right: 8px;
	opacity: 0.7;
}

.legend img {
	width: 18px;
	height: 18px;
	float: left;
}

.legend p {
	font-size: 12px;
	line-height: 18px;
	margin: 0;
}
First, we set properties for the legend on the whole using .legend to style the legend class. We set a line height, color, font, padding, background, drop shadow, and border corner radius. Next we set our icon (i) tag, this should be set to float: left; so that elements will align into columns, then we set properties for the image (img) tag, making them the same size and giving them the same float as the icons. Lastly, we style our paragraph tag (p), making sure line-height is consistent with the others. Save and refresh your map. You should see your styled legend applied to your map. View Example on its own (take a look at Page Source) This is not the only way to create a legend. An alternative method uses a For loop conditional statement to add legend elements based on the number of data classification categories you have. A legend section of the choropleth tutorial at the Leaflet homepage shows this, take a look.

b. Add a Scale Bar

The Leaflet Control object allows you to add a number of elements, including attribution and zoom controls. One easy component to add is a scale bar. In your script, enter the following line to add a scale bar to your map.
// Add Scale Bar to Map
L.control.scale({position: 'bottomleft'}).addTo(map);
Save and refresh. The current state of our map: View Example on its own (take a look at Page Source)

6. Custom Basemaps: Creating Tile Layers

If the provided basemaps do not work to your liking, you can created custom tiles that can be served to your maps. This topic, on the whole, is large and we will have another session that introduces creating tile layers, stay tuned. To whet your appetite on tiles, here are a few key considerations for tile layers.

How to Create and Style Tiles

Mapbox is a company that specializes in tile layers, and maintains two pieces of open-source software that can be used to create tiles for your maps. For beginners, I especially recommend Tilemill.
  1. Tilemill: https://www.mapbox.com/tilemill/
  2. Mapbox Studio: https://www.mapbox.com/mapbox-studio
Both of these can be hosted by Mapbox in their Cloud or on a geoserver. You can also create Tile Map Services and Web Map Services that hold tiles.

Hosting and Serving Tiles

Hosting can be the tricky part when it comes to tile layers. Worldwide tile sets are VERY LARGE. Localized areas are more accessible. When you are getting started, I suggest using a cloud service such as Mapbox to host your tiles.

What Elements are Best Represented on Tiles

Any element that does not require interaction or will remain relatively static can be put on a tile. Individual data pieces are heavy. As a general rule of thumb, the more features you can include on your tiles, the faster your map will perform.

7. Style your Interface

Let's finish today with some interface customization. Let's just do two simple things to further customize our interface, change the font, and right justify the credits.

a. Change the Fonts

Choosing fonts is an important part of cartography, and an often overlooked one. Right now, our map uses the default Browser font, usually Times New Roman. To edit fonts, we want to style CSS. In CSS, there are a lot of options for fonts, for more reading, check out the w3schools font documentation.

Link a Font from Google Fonts

Traditionally, a font is loaded into your page only if you have it on your computer. This presents a problem though, if someone doesn't have the font, it will change the page to use secondary or default fonts. In order to ensure that every visitors computer display the same, you can link to online font libraries. A common, useful online font library is Google Fonts. Google fonts can be added to any site, and since you link to the style, you don't have to worry about the user not having the font installed on their computer. Check out the Google Font library and explore their options. Let's link a common web font called Open Sans to our document so we can use it. To link it to our document, enter the following line of code into the head section of your document. It should go right after your stylesheets.
<head>...
<link href='http://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
...</head>
Next, to style all text in our document with the Open Sans font, modify the body tag in the CSS (the code between the style tags). Modify the body CSS properties to look like the following, adding a font-family property after margin.
body {
    margin: 0px;
    font-family: 'Open Sans', Helvetica, sans-serif;
}
Save and refresh your map. Open Sans will now be your preferred font!

Style the Credits using CSS

Lastly, to help you explore the power of CSS, style the credits at the bottom of your page. Because the div containing the credits has id="credits", we can style it using #credits. All of the contents in the credits div are between to paragraph tags. CSS styling is written in a nested fashion, to style everything that is in a p element within the #credits div, we use #credits p. Add the following snippet between the style tags in the head section of the document.
#credits p {
	margin-top: 5px;
	font-size: 12px;
	text-align: right;
	line-height: 16px;
}
Save and refresh your map. It will look like the following!

View Example on its own (take a look at Page Source)

Challenge!! Can you add some interactivity by using JavaScript to implement buttons, checkboxes, and toggles?

Concluding Remarks

Cartographic styling, including map symbolization and adding map elements, is an important component of web mapping. Read more on the documentation at the following sites.



Return to DUSPVIZ tutorials page