Typing up the coordinates for the last homework assignment was, I imagine, not a lot of fun. It’s also silly for me to assert, in the chapter before last, that it’s important to separate data wrangling from programming logic, only to then have you run them together again. But the data structure for representing geographical Objects in JavaScript, GeoJSON, is complex enough to warrant its own chapter. That is this chapter, and in it we’ll be doing a bit of detective work on a short poem in order to generate our dataset.

This chapter also marks the beginning of the end of the course, as we’ll be working on this dataset, and the “‘Could Be’ Project,” for the rest of the chapters. We’ve come so far in so short a time. In my class, you would already be starting on your own final project, so I use the “Could Be” Project as an opportunity to teach new techniques for building that project. If you want to see the “finished” project, have a look here. It’s not much because the text I’ve written is so thin, but you can at least see what you, too, will be capable of.

Langston Hughes’s “Could Be”

The “Could Be” Project is a look at Langston Hughes’s poem, “Could Be.”1 The complete text is available at Song of America, so I encourage you to read it there. In 16 lines of poetry, Hughes manages to refer to five different geographical spaces in the United States, with two of them being mentioned twice apiece. The places he mentions are:

  • Hastings Street
  • Lenox Avenue
  • 18th & Vine
  • 5th & Mound
  • Rampart

I was thinking about this poem in the Spring, and I wondered if mapping it might open up any sort of new interpretive angles into it. It’s also a good poem for teaching digital mapping, because the amount of data is small but important to the text as a whole. Your own projects will have considerably more data.

Yet in converting a poem into data, a scholar has to at the same time wonder how that data should look. Remember, data are captured, so what information should we capture when trying to represent this poem?

Geographical data structure

For these five locations, clearly I need their names, so that I can tell them apart. I also need their coordinates, if I plan on mapping them. In other words, I’m already thinking about the data in terms of JavaScript Objects and properties. Every Place Object will have a .name property and a .coordinates property. Better: it’ll have both a .lat property for its latitude and a .lng property for its longitude.

What else could I include? How about how many times each place is mentioned? OK, that’s a .mentions property. Then maybe the line number? OK, that’s the .line property. But wait, two places are mentioned twice. How will the .line property work there? Maybe .line should be an array, then, or .lines. Finally, if the place has a Wikipedia article related to it, we can include that link as the .wikipedia property.

A way of thinking about Objects and properties that might be familiar to you is as rows and columns in a spreadsheet. That is, each row is its own Place Object, and each column is a property associated with it. In fact, that’s what I did for this poem. It turns out that filling in the data is pretty easy for each property except .lat, .lng, and .wikipedia. Those require some digging.

Geo-sleuthing

So how to fill in those coordinates? There are some immediate low-hanging fruit, luckily. Lenox Avenue clearly refers to the avenue that runs through Harlem. Similarly, 18th & Vine is a cradle of jazz in Kansas City. Wikipedia even gives coordinates for the latter location, so I put those in the spreadsheet.

Lenox Avenue, on the other hand, is a long avenue. How can one point capture all of it? One option would be to draw a line, of course. The other option would be to arbitrarily select a point on the line. If you feed “Lenox Avenue” into Google Maps, for example, it makes that arbitrary decision for you and gives you coordinates between 127th and 128th Streets. That feels about right.

Rampart provides a different obstacle. It’s a reference to Rampart Street in New Orleans, so that sorts out the .wikipedia property, but the coordinates Wikipedia gives are bonkers. They are for the intersection of St. Philip St. and N Prieur St., about ten blocks away from Rampart. As the Wikipedia article notes, the intersection of Rampart and Canal was a center of African-American life in New Orleans, and as we’re already seeing, Hughes is referring to various urban centers of African-American life. So, instead, I drop a marker at that intersection and make note of those coordinates.

Hastings Street was the center of African-American life in Detroit’s Black Bottom neighborhood, but there’s a problem. The street doesn’t really exist anymore, having been cleared away to make space for Interstate 75. I looked up old maps of Detroit and deduced the latitude and longitude for Hastings Street that way.

5th & Mound is trickier still. It, too, doesn’t exist anymore, the intersection having been destroyed to make way, again, for Interstate 75. Nevertheless, 5th & Mound had been part of Cincinnati’s West End, which remains the most African-American area in the city. Not much is available about this neighborhood online, but the Cincinnati History Library and Archives provides a search tool that lets the user browse their Kenyon Barr Collection, which features a series of creepy photos of the neighborhood, taken as the city was preparing to raze it to the ground. Furthermore, as Steven C. Tracy notes in a history of the Blues in Cincinnati, the Cotton Club, the most important venue for African-American entertainers, was only a block away, on 6th and Mound. Either way, Mound Street still exists, as does 5th, but they no longer intersect. I extrapolated their intersection and added those coordinates to the dataset.

The completed dataset, small as it is, is available as a Google Sheets document. Building out the data in a spreadsheet made the data entry straightforward, but, unfortunately, Leaflet can’t simply read a spreadsheet. We need an intermediate step.

The GeoJSON format

You learned about JSON when working with Chaucer’s General Prologue back in Chapter 9. A subset of it, GeoJSON, is a handy way to describe geographical data, one that Leaflet understands. In fact, we can describe our Hastings Street point in GeoJSON like this:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [-83.0370, 42.3340]
  },
  "properties": {
    "name": "Hastings Street",
    "html": "Hastings Street",
    "tab": "hastings-street",
    "mentions": 2,
    "lines": [1, 13],
    "wikipedia": "https://en.wikipedia.org/wiki/Black_Bottom,_Detroit"
  }
}

Our point is a Feature Object in GeoJSON. Notice, however, that the coordinates are flipped. Instead of [lat, lng], like in Leaflet, the coordinates here are [lng, lat]. Instead of having an endless list of potential properties, the Feature Object has three, a .type, a .geometry, and its own .properties Object. That Object’s properties are where we can stash our own properties like .wikipedia. I made the .lines property an array by enclosing it in brackets. The .tab and .html properties will become important in Chapter 14.

With as small a dataset as the one we have for “Could Be,” generating a GeoJSON file “by hand” would not be too difficult. However, there are online tools that convert spreadsheets to GeoJSON. Convert CSV, for example, lets you even paste in the data you copy from a spreadsheet. In the second step, you note whether the first row is column headers (typically yes), and in the third step, you mark which two columns feature latitudes and longitudes.

The result is available here. As you can see, the Feature Objects are collected into an array that is the .features property of a FeatureCollection Object.

GeoJSON in Leaflet

It’s time to start putting all this into a real project. Create a file in your project in Atom, called could-be.html, and paste in this basic structure:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>“Could Be,” by Langston Hughes</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" integrity="sha512-M2wvCLH6DSRazYeZRIm1JnYyh22purTM+FDB5CsyxtQJYeKq83arPe5wgbNmcFXGqiSH2XR8dT/fJISVA1r/zQ=="
   crossorigin=""/>
    <link rel="stylesheet" href="leaflet.css" />
  </head>
  <body>
    <div class="container">
      <h1>“Could Be,” by Langston Hughes</h1>
      <div id="could-be-map" class="map"></div>
    </div>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha512-lInM/apFSqyy1o6s89K4iQUKg6ppXEgsVxT35HbzUupEVRh2Eu9Wdl4tHj7dZO0s1uvplcYGmt3498TtHq+log==" crossorigin=""></script>
    <script src="could-be.js"></script>
  </body>
</html>

This is identical to the leaflet.html, except the content is a bit different, and it’s loading could-be.js instead of leaflet.js. Furthermore, the map’s <div> is called #could-be-map.

Set up could-be.js similarly to leaflet.js:

let map, tileLayer;
map = L.map("could-be-map");
tileLayer = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png", {
              attribution: "&copy; <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> &copy; <a href='http://carto.com/attribution'>CARTO</a>",
              subdomains: "abcd",
              maxZoom: 18
            }).addTo(map);
map.setView([40.730833, -73.9975], 16);

Save and open could-be.html in the browser. If the map appears, go ahead and commit. You can load the GeoJSON just like we loaded regular JSON, using the jQuery $.getJSON() method. Now, Leaflet offers an L.geoJSON() method that would make adding the GeoJSON one line of code. But, trust me on this, it will be easier to do a bit of extra work here and avoid using that method. Instead, we’ll create an array of objects, couldBeFeatures, where each object has the properties of each of the GeoJSON features. So, add to could-be.js:

// Define the features array.
let couldBeFeatures;
$.getJSON("https://the-javascripting-english-major.org/could-be.geo.json", function(data){
  // Define the Leaflet layer.
  let couldBeLayer;
  // Iterate over the .features property of the GeoJSON object to
  // create an array of objects (features), with every object’s
  // properties as noted.
  couldBeFeatures = data.features.map(function(feature){
    // This return returns an object.
    return {
      name: feature.properties.name,
      html: feature.properties.html,
      tab: feature.properties.tab,
      mentions: feature.properties.mentions,
      lines: feature.properties.lines,
      wikipedia: feature.properties.wikipedia,
      // Create an L.latLng object out of the GeoJSON coordinates.
      // Remember that in GeoJSON, the coordinates are reversed
      // (longitude, then latitude).
      latLng: L.latLng(feature.geometry.coordinates[1], feature.geometry.coordinates[0])
    };
  });
  // Now create a Leaflet feature group made up of markers for each
  // object in couldBeFeatures.
  couldBeLayer = L.featureGroup(couldBeFeatures.map(function(feature){
    return L.marker(feature.latLng);
    })
  )
  // Add the layer to the map.
  couldBeLayer.addTo(map);
  // Redraw the map so that all the markers are visible.
  map.fitBounds(couldBeLayer.getBounds());
  // Zoom out one level to give some padding.
  map.zoomOut(1);
});

Notice that, except for the definition of couldBeFeatures, all of the Leaflet work is happening inside the callback function, because $.getJSON() is async.2 I also introduce three new methods here. .getBounds() returns the bounding box that contains the entirety of a layer, in this case our couldBeLayer. That is fed as a parameter to .fitBounds(), which changes the map Object’s state to a new zoom level and center coordinate. Then I use the map Object’s .zoomOut() method to zoom out a smidge to make all the markers appear on the map. couldBeLayer, in the meantime, is a Leaflet feature group Object, as it is made up of several markers.

Save and reload. Your map should now show the whole United States and feature five markers, one over New York, one over Cincinnati, one over Kansas City, one over Detroit, and one over New Orleans. Otherwise, catch up with the work I have done so far over here in oder to see what the project looks like.

Exercises

  1. Design the data structure of your own final project and begin collecting data for it in a spreadsheet.
  2. Create new HTML and JavaScript documents for your project and get your own personal GeoJSON data, or at least as much as you have, plotted.

Footnotes

  1. Alba Newmann Holmes introduced me to this poem when presenting a paper, “‘Could Be’: Langston Hughes as Situationist Cartographer,” at a special session I convened at the MLA Convention in 2015

  2. couldBeFeatures is defined outside of the .getJSON() method. Defined the way it is, it has “global” scope, meaning that it will be available in later functions, as we’ll see in Chapter 14