Mapping Examples

There are many ways to make maps, but simple versions are:

  1. Use an SVG file with shapes for each country or state, or
  2. Plot a set of latitude/longitude points

Mapping Shapes

Download this example here.

This example loads a map from an SVG file and adds it to the web page so that it can be controlled by your p5.js sketch. Inside the SVG file, each boundary will be named. For instance, in the world maps, it'll be the 2-digit country code for that area.

This part of the code takes care of loading and sizing a world map that uses the Robinson projection:

let worldMap;


function setup() {
  createCanvas(720, 480);
  background(224);
  
  // If you change the dimensions, the aspect ratio will stay the same. 
  // The browser will size the map to use as much of the width/height as possible.
  let mapWidth = width * 0.8;  // use 80% of the sketch size
  let mapHeight = height * 0.8;
  // Center the map on the screen. The mapX and mapY 
  // coordinates are relative to the sketch location. 
  let mapX = (width - mapWidth) / 2;
  let mapY = (height - mapHeight) / 2;

  let mapPath = "data/world-robinson.svg";
  //let mapPath = "data/world-equirectangular.svg";
  //let mapPath = "data/us-counties.svg";
  //let mapPath = "data/us-states.svg";
  
  // This will create a new SVG map from the 'robinson.svg' file in the data folder.
  // Once the map has finished loading, the mapReady() function will be called.  
  worldMap = new SimpleSVG(mapPath, mapX, mapY, mapWidth, mapHeight, mapReady);
}
The mapReady() function is optional, but gives us a way to find out more information about the map, and tell the map what to do when the user clicks on a shape, or the mouse moves over a shape. It also lists all the names of the shapes that it found in the file, so you can see that the map is working like you expect.
// this function is called when the map loads
function mapReady() {
  // show a list of all the shapes by name (i.e. all the 2-digit country codes)
  print(worldMap.listShapes());
  
  // call the function named 'mapClick' whenever a shape is clicked 
  worldMap.onClick(mapClick);
  
  // handle mouseover (hover) events, and mouseout (the opposite of hover)
  worldMap.onMouseOver(mapOver);
  worldMap.onMouseOut(mapOut);
}

In the map files, there are some sections that aren't countries or states: they might be boundary lines or the ocean. Since we don't want these to light up on hover (or be clickable), we create a function that checks whether we can ignore that particular area:

// returns 'true' if this shape should be ignored
// i.e. if it's the ocean or it's the boundary lines between states
function ignoreShape(name) {
  return (name === 'ocean' || name.startsWith('lines-'));
}

When a country on the map is clicked, this code checks to see if its something to be ignored, and if not, sets the fill color to red. It also prints the id (name) of the shape to the console:

function mapClick(shape) {
  if (!ignoreShape(shape.id)) {
    worldMap.setFill(shape, 'red');
  }
  print(`click ${shape.id}`);
}

Mouse hovers (also called rollovers) are handled in a similar way. We check whether it's a shape to be ignored, and if not, set its fill color:

function mapOver(shape) {
  if (!ignoreShape(shape.id)) {
    worldMap.setFill(shape, '#666');
  }
  print(`over ${shape.id}`);
}


function mapOut(shape) {
  if (!ignoreShape(shape.id)) {
    worldMap.setFill(shape, '#ccc');
  }
  print(`out ${shape.id}`);
}

These handlers use the setFill() function that's part of SimpleSVG. Unfortunately, it's not as simple as saying shape.fill = 'red' because each country shape might have several sub-shapes to it. The SimpleSVG code hides this complexity and just takes care of it for you.

Other Maps

There are several maps included in the data folder of this sketch:

Each of these have been modified slightly to rearrange layers or rename elements so that they're consistent with one another, and work well with SimpleSVG.

They take up quite a bit of space, so remove the ones that you're not using.

Most of the work is done inside the SimpleSVG.js file. You can see the id for each shape by looking at the SVG inside Adobe Illustrator, where each path/group will be named in the layers palette. The SimpleSVG code will only pay attention to the first layer of named elements in the file, so make sure that's where your named entries are. You may need to move things around a bit.

Mapping Points

Download this example here.

To map a set of points, create a CSV or TSV file with a columns for latitude and longitude. This example uses postal codes and their lat/lon location. To read the file:

function setup() {
  createCanvas(720, 452);
  
  // load the table of zip codes and call the tableLoaded() function when ready
  loadTable("data/zips.tsv", "header", "tsv", tableLoaded);
}
Once the file is ready, it will call the tableLoaded() function, which goes through the file line by line, and creates an array of "places", each with an x/y position and a name:
function tableLoaded(t) {
  // go through each row of the zip codes file
  for (let row = 0; row < t.getRowCount(); row++) {
    let place = { };
    place.lat = t.getNum(row, "lat");
    place.lon = t.getNum(row, "lon");
    place.name = t.getString(row, "name");
    place.zip = t.getString(row, "zip");
    
    // now calculate specific location
    let projected = projectAlbers(place.lon, place.lat);
    // or you can use the Mercator version:
    //var projected = projectMercator(place.lon, place.lat);
    place.x = projected.x;
    place.y = projected.y;
    placeList.push(place);
  }
  // print out how many points were found
  //print(placeList.length);
  
  findMinMax();
}
At the end of that function, it calls findMinMax() which figures out the minimum and maximum coordinates, which are needed to draw the shapes to the screen:
// figure minimum and maximum values for the x- and y-coordinates
function findMinMax() {
  // start by setting the min/max to the first point in the list
  minX = maxX = placeList[0].x;
  minY = maxY = placeList[0].y;
  // then check each of the other points in the list
  for (let i = 1; i < placeList.length; i++) {
    var place = placeList[i];
    if (place.x > maxX) {
      maxX = place.x;
    }
    if (place.x < minX) {
      minX = place.x;
    }
    if (place.y > maxY) {
      maxY = place.y;
    }
    if (place.y < minY) {
      minY = place.y;
    }
  }
}
Finally, drawing to the screen is just a matter of iterating over the placeList array and drawing each point:
function draw() {
  background(250);
  stroke(128);
  
  // draw each of the points on the screen
  placeList.forEach(function(place) {
    let x = map(place.x, minX, maxX, 0, width);
    let y = map(place.y, maxY, minY, 0, height);
    point(x, y);
  });
}

Mapping Points and Using the Mouse

Download this example here.

This example builds on the previous but uses mouse input to handle hovers or clicking individual points. This is done by rewriting the draw() method so that we can keep track of what dot is closest to the mouse. We use the dist() function to calculate the distance from the mouse to each point, and if it's less than the last value, closestPlace is set to that place entry.

// the place currently under the mouse
var closestPlace = null;


function draw() {
  background(250);
  stroke(128);
  
  // for rollovers/selection, keep track of the point nearest the mouse
  cursor(CROSS);  // better for selection
  let closestDist = 5;  // minimum distance
  closestPlace = null;
  
  placeList.forEach(function(place) {
    let x = map(place.x, minX, maxX, 0, width);
    let y = map(place.y, maxY, minY, 0, height);
    point(x, y);
    
    // check if the distance from the mouse to this point is the closest so far 
    let mouseDist = dist(mouseX, mouseY, x, y);
    if (mouseDist < closestDist) {
      closestPlace = place;
      closestDist = mouseDist;
    }
  });
  
  if (closestPlace !== null) {
    let x = map(closestPlace.x, minX, maxX, 0, width);
    let y = map(closestPlace.y, maxY, minY, 0, height);
    noStroke();
    fill(255, 0, 0);
    ellipse(x, y, 5, 5);
    fill(0);
    textAlign(CENTER);
    text(closestPlace.name, x, y - 5);
  }
}


function mousePressed() {
  if (closestPlace !== null) {
    // do something with 'closestPlace' here
    print(`${closestPlace.name} clicked`);
  }
}

After drawing all the points, the code checks if (closestPlace !== null), and if so, it'll re-draw that point using an ellipse, and write its name above it.

We also add a mousePressed() function, which you can use to trigger something more interesting when clicking on a point. Because closetPlace is set inside draw(), that'll be the place being clicked.

Projections

Over in the projections.js file, there's code to convert latitude and longitude points to an Albers Equal-Area Conic or to Mercator coordinates. Albers is what's used most often for the US, while Mercator is easy to calculate and gets used more often than it should. You needn't worry about that code unless you want to use other kinds of projections, or you're curious about how they work.