Make a Web Map with Leaflet

Welcome to the third Glitch Challenge! Challenges are here to help you build an app on Glitch, regardless of where you are in your coding journey. In this article you’ll find a neat starter app for you to use as a jumping off point to create an app, and a list of challenges to make the app your own. In this challenge we'll show you how to make a web map with Leaflet!

Leaflet is a JavaScript library for making interactive web maps. This library allows you to create a map container, add a base map, and add interactive features on top of the map — plus panning and zooming. The base map is made of tiles, or small images that each represent a specific geographic location. The interactive features, or “data layer,” consists of geospatial vector data — points, lines, and polygons with associated geographic information. Collectively, this kind of map is known as a slippy map.

For the Leaflet Starter App, we created a map showing neighborhood boundaries in Portland, Oregon. In Portland there are neighborhood coalitions as well as neighborhood associations, so the data is styled based on which neighborhood coalition the association belongs to. It also has interactive elements — neighborhood associations are highlighted when hovering over them, and popups show the name of the neighborhood association when clicked.

In the next section, we describe how the code in this app works. Feeling comfortable with the app? Skip down to “The Challenge” and get started!

How it works

The code powering this app is in index.html, style.css, script.js, and in the assets directory.

assets

The data for this map comes from the City of Portland as a GeoJSON file. GeoJSON is a specific flavor of JSON that is used to describe geographic vector features (points, lines, and polygons). We downloaded this file and then uploaded it to the assets directory in our Glitch app so we can request the data with JavaScript when creating the map.

Note: the data is in the assets directory because that's where files go automagically in Glitch when you add them to your project. It's also handy because the assets directory gives you a URL for accessing each resource, which we need later in the code!

index.html

index.html contains the main structure of the page as well as any additional files the app needs to run effectively. In the <head> section we add links to the Leaflet JavaScript and CSS files and in the <body> we add an empty <div> with id="mapid”.

<div id="mapid"></div>

style.css

In order for the map to render, the <div> element housing it must have a height. We can make the height responsive by setting html and body to height:100% and then giving #mapid a percentage value for height.

html, body {
  height: 100%;
  width: 100vw;
}

#mapid {
  margin-top: 5em;
  height: 50%;
}

script.js

script.js is where we actually make the map, add layers to it, and add event handlers to make the map interactive. First things first — we have to make a map!

The file starts by creating a L.map object, which takes two parameters: the id of the <div> where the map should live on the page, and an optional object to specify more options. In this case, we tell the map to put itself in the ”mapid” <div> and set the map’s initial center point and zoom level to center on Portland.

// make the map
let map = L.map("mapid", {
  center: [45.55, -122.65], // latitude, longitude in decimal degrees (find it on Google Maps with a right click!)
  zoom: 10 // can be 0-22, higher is closer in
});

Now we start adding things to the map, with the base map tiles up first. For this map we are using the Stamen Toner tileset (lite version). We create an L.tileLayer, point it to the URL of the tile server, and add it to the map.

// add the basemap tiles
L.tileLayer(
  "https://stamen-tiles.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}@2x.png"
).addTo(map); // remember, `map` is pointing to the `L.map` object

At this point, we have a map that pans and zooms around and a base map to look at while you pan around. Great! But that’s not the point of our map — the point of our map is to show neighborhood associations in Portland. But before we add the data, we have to do a little bit of setup work by defining some event handlers.

Event handlers are functions that respond to events on the page. These are defined before we load in the data so they can be attached to the data layer and triggered on specific events. We create four event handler functions: highlightFeature, resetHighlight, zoomToFeature, and onEachFeature, which attaches the other three functions to specific mouse events as they relate to the data on the map.

let geojson; // this is global because of resetHighlight

// change style
function highlightFeature(e) {
  let layer = e.target; // highlight the actual feature that should be highlighted
  layer.setStyle({
    weight: 3, // thicker border
    color: "#000", // black
    fillOpacity: 0.3 // a bit transparent
  });
}

// reset to normal style
function resetHighlight(e) {
  geojson.resetStyle(e.target);
}

// zoom to feature (a.k.a. fit the bounds of the map to the bounds of the feature
function zoomToFeature(e) {
  map.fitBounds(e.target.getBounds());
}

// attach the event handlers to events
function onEachFeature(feature, layer) {
  layer.on({
    mouseover: highlightFeature, // a.k.a. hover
    mouseout: resetHighlight, // a.k.a. no longer hovering
    click: zoomToFeature // a.k.a. clicking
  });
}

Because the neighborhood association data we’re using is in a different file, it must be retrieved asynchronously. This ensures that all of the data has been loaded before trying to use it (in this case, add it to the map). For the most part, you’re going to want to leave this section alone, except to change the URL so it matches your data. The important thing to know here is that once the data is loaded, the doThingsWithData function is called.

// get the data
fetch(
  "https://cdn.glitch.com/4e131691-974a-4b1f-95e5-47137b94043d%2FNeighborhood_Boundaries.geojson?1553538254953" // this URL is provided in the assets directory
)
  .then(function(response) {
    return response.json();
  })
  .then(function(json) {
    // this is where we do things with data
    doThingsWithData(json);
  });

(If you're interested to know more about how we're loading the data, you can read more about using Fetch)

doThingsWithData is where we (yup you guessed it) add the data to the map. In this case, we want to style the neighborhood associations based on their neighborhood coalition (a coalition consists of multiple neighborhood associations). To do that, we create a variable colorObj and assign to it the returned value from assignColors — an object assigning each unique coalition to a color.

// create an object where each unique value in prop is a key and
// each key has a color as its value
function assignColors(json, prop) {
  // from ColorBrewer http://colorbrewer2.org
  let colors = [
    "#a6cee3",
    "#1f78b4",
    "#b2df8a",
    "#33a02c",
    "#fb9a99",
    "#e31a1c",
    "#fdbf6f",
    "#ff7f00",
    "#cab2d6",
    "#6a3d9a",
    "#ffff99",
    "#b15928"
  ];
  let uniquePropValues = []; // create an empty array to hold the unique values
  json.features.forEach(feature => { // for each feature
    if (uniquePropValues.indexOf(feature.properties[prop]) === -1) { 
      uniquePropValues.push(feature.properties[prop]); // if the value isn't already in the list, add it
    }
  });
  let colorObj = {}; // create an empty object
  uniquePropValues.forEach((prop, index) => { // for each unique value
    colorObj[prop] = colors[index]; // add a new key-value pair to colorObj
  });
  return colorObj;
}

The second part of doThingsWithData adds the data to the map with L.geoJSON by passing the data as the first parameter. The second parameter is an optional object containing additional specifications — we use it here to set the style for each feature and add the event handlers. We then call two methods on the L.geoJSON layer — bindPopup, which adds a popup to each feature, and addTo(map) which adds the layer to the map.

// once the data is loaded, this function takes over
function doThingsWithData(json) {
  // assign colors to each "COALIT" (a.k.a. neighborhood coalition)
  let colorObj = assignColors(json, "COALIT");
  // add the data to the map
  geojson = L.geoJSON(json, {
    // both `style` and `onEachFeature` want a function as a value
    // the function for `style` is defined inline (a.k.a. an "anonymous function")
    // the function for `onEachFeature` is defined earlier in the file
    // so we just set the value to the function name
    style: function(feature) {
      return {
        color: colorObj[feature.properties.COALIT], // set color based on colorObj
        weight: 1.7, // thickness of the border
        fillOpacity: 0.2 // opacity of the fill
      };
    },
    onEachFeature: onEachFeature // call onEachFeature
  })
    .bindPopup(function(layer) {
      return layer.feature.properties.NAME; // use the NAME property as the popup value
    })
    .addTo(map); // add it to the map
}

To recap:

  • assets is where we add the GeoJSON data file
  • index.html contains an empty <div> with id=“mapid”
  • style.css gives our ”mapid” <div> a height
  • script.js creates the map, adds the base map layer and data layer, and adds interactive elements/event handlers (popups, highlighting, zooming)

The Challenge

In case you haven’t done a Challenge with us before, here are the basics:
  1. Remix this app
  2. Take a look at our challenge prompts below and pick one (or more!) to tackle
  3. Give it a try! You can always reach out for help on Twitter or via our support forums.
  4. Submit your awesome app (yes I already know it’s awesome) using the form below.

For this challenge, you have several options. Each challenge concerns a different part of the map — if you’re feeling up to it, try all of them!

  • Make the popups look pretty — add some styling, change the names to title-case, etc.
  • Some Portland neighborhoods have overlapping boundaries — the dataset used in this map has these areas as separate polygons. Use styling to make it clear that these are overlap areas.
  • Add a legend to this map.
  • Change out the GeoJSON file for a file of your choosing, ideally in a different location. Think about what might be different between the two datasets. (Data.gov is a great place to find GeoJSON data!)
  • Leaflet has some special things you can do when adding markers to a map. Add some point data and do some cool marker stuff.

After you’ve made your super radical map, submit it using the form below. We are so looking forward to checking out your amazing work! If you have any questions or need help, reach out on Twitter @glitch or via our support forums.

Tune in to a live stream next week on Tuesday, April 2nd at 10:30am Pacific/1:30pm Eastern to watch us tackle the challenges ourselves and showcase some of the super cool apps you've made.