Multivariate Mapping

In April, I a tutorial I led for the UCSF Family and Community Medicine Residents where we created maps of ED visits in California related to asthma (slides available: here; asthma map available: here). It's very powerful to view this information on a map, rather than in a table, where it's nearly meaningless unless you have your zip codes memorized.

But what if we wanted to compare this information to another variable, say income. Do zip codes with higher median household income, in general, have higher rates of asthma? One way of doing this is to create two maps (one for income, one for asthma) and hold them side-by-side, comparing each zip code individually. But that doesn't let us quickly query our map or really explore our data in much depth.

Enter Crosslet.js, a powerful and fun Javascript library that combines three other useful libraries: D3, Leaflet, and Crossfilter. Crosslet lets us connect our map to multiple variables for each polygon (in this case being zip code) and query across all of them simultaneously.

Check out the example below. Use the filters on the top right to select higher household incomes. Then, drag the blue box to the left and watch the histogram for asthma visits. See how the histogram showing asthma visits shifts to the right as we select poorer zip codes? I also included the percent of the population that identified as Black or African American in the latest census. This lets us quickly view the relationship between race and income, race and asthma, or all three variables together. (Stay tuned for a tutorial on how to scrape this data from sites like ZipAtlas using a simple and powefful API-generating tool called Kimono).

OpenStreetMap Africa

OpenStreetMap (OSM) is a free map of the world built by a dedicated (and growing) network of mappers. Since 2004, hundreds of thousands of mappers around the globe have mapped 25 million miles of roads, over 130 million buildings, and millions more water fountains, schools, cafés, rivers, coastlines, helipads and hundreds of other features.

Here is southern Africa, OSM has grown exponentially over the past 5 years and now it provides incredible coverage of many countries in the region. Check out the interactive map below by Mapbox to see the growth of OSM in this region. In particular, watch the growth in smaller, secondary roads from 2013 to 2014. It's this type of data that can dramatically improve estimates of travel time to health clinics for people living in rural areas. You can see on this website that shows edits in the past 90 days, that roads are constantly being added and updated, leveraging local knowledge and the power of a distributed network of volunteers.

As you can see, OSM has major planetary coverage. And whether you've known it or not, you've probably seen the map before: Craigslist, Foursquare, Wikipedia, Flickr, and even Apple Maps all draw from OSM data (note: Apple also uses TomTom and data sources as well).

Interested in tracing your home, streets in your neighborhood, or tracing satellite images of the roads and places that are important to you? Want to get involved in humanitarian mapping to aid relief efforts? Head over to openstreetmap.org to get started.

Arrival!

Well, after one British Airways chicken tikka masala, two sunsets and two sunrises seen from a window-seat, and over 30 hours of travel, I’ve arrived at my home in South Africa. Though the trip was long, I made it in two long legs - from San Francisco to London, then London to Johannesberg. Both legs were aboard the new Airbus A380 - by far the comfiest, quietest and smoothest ride I've ever had. Incidentally, the A380 is also equipped with an incredible flight-tracker that lets you view the plane’s location in real-time from any angle, aspect, and zoom. I tracked our flight as we passed over the arctic, skirting the tip of Greenland during the first ice-blue sunrise, then much later over Botswana for the second sunrise - this time a dusty pinot red as we crossed the Kalahari Desert.

Arctic sunrise over the North Atlantic

Arctic sunrise over the North Atlantic

And since this is intended to be a blog about mapping, I'm compelled to say a few things about our flightpath:

First is that flightpaths can be predicted by drawing the shortest line possible on a globe between two points, a path known as a Great Arc. Try to imagine taking a globe, slicing a plane through the origin, destination and center of the earth, then tracing the intersection of the plane and the globe. The resulting line you traced along the earth's surface is the Great Arc.

If you think about Great Arcs and direction long enough, you can start to come to some confusing and counterintuitive conclusions. Like the fact that a Great Arc is not, strictly speaking, "straight" even though it would lead you in a complete path around the globe if you followed it far enough. Pilots have to constantly adjust their bearing during flight. In contrast, if you were a sailor who set your compass to London and followed the same bearing indefinitely, it would take a bit longer and, more importantly, if you kept going on the same course, you would never return to the same point. That is, it is not a Great Circle. Rather, you'd slowly spiral toward the north (or south) pole, tracing a path known as a loxodrome.

If we trace the Great Arcs of my flight to South Africa on a flattened, rectangular image of the world (like Google Maps), it looks a little like the two flights had nearly perpendicular paths, with the first flight goes east, then a sharp turn South after departing London:

SF --> London -->J ohannesberg flightpaths in a equirectangular (Plate Carrée) projection. (more cool descriptions on this map projection: here)

SF --> London -->J ohannesberg flightpaths in a equirectangular (Plate Carrée) projection. (more cool descriptions on this map projection: here)

The graphic above illustrates an important phenomenon: that in order to make maps, we have to "project" the 3D surface of the planet onto a 2D image. And that these projections can occasionally be misleading. The equirectangular projection, as shown above, is relatively skewed since the flight from SF to London skirts the Arctic circle.

One alternative is to view the word through an orthographic perspective. This gives us a view of the globe as we would expect to see it from space - with minimal distortion in the center (in this case, London) and increasing distortion of shapes as we approach the edges of the circle. From this view, we see that our path from London to Johannesberg is, in fact, nearly a continuation of the line we traced from SF to London. These two flightpaths aren't perpendicular at all:

  London-centric orthographic projection of the great arc paths that trace my flightpath.

 

London-centric orthographic projection of the great arc paths that trace my flightpath.

And here are both types of maps in an interactive side-by-side tool. Try dragging the "flat" map and you can see the earth rotate in sync. [note: this map uses WebGL, which works best on a modern browser. It may also be hard to view on your phone.]

There are many other types of projections, all of which provide a different flattening of 3D into 2D. Below are a few more examples. For people interested in learning more about projections, I'd recommend this great article by Mapthematics, and their comprehensive library of projections.

Goode homolosine projection

Peirce quincuncial projection

Equatorial Stereographic Azimuthal Projection 

I'll leave you with this little map of Pretoria, with markers showing the Foundation of Professional Development and the University of Pretoria, close to where I am now living.


Extra Note (June 5th, 2015)

I just learned that the Arc.js library can actually map Great Arcs for you. Thanks to a little help from a great Mapbox Example, I was able to animate my flight path using this tool:


Here is my code for the flight animation, most of which is taken from the example listed above:

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8 />
        <title>Animating flight paths</title>
        <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
        <script src='https://api.tiles.mapbox.com/mapbox.js/v2.1.9/mapbox.js'></script>
        <link href='https://api.tiles.mapbox.com/mapbox.js/v2.1.9/mapbox.css' rel='stylesheet' />
        <style>
            body { margin:0; padding:0; }
            #map { position:absolute; top:0; bottom:0; width:100%; }
            .leaflet-left {
                display: none;
            }
        </style>
    </head>
    <body>


        <!-- We use arc.js to make our paths curved. -->
        <script src='https://api.tiles.mapbox.com/mapbox.js/plugins/arc.js/v0.1.0/arc.js'></script>
        <!-- This is our data file - it's an array of [[lat,lng],[lat,lng]] pairs
that define starting and ending locations of flight paths -->
        <script src='./data.js'></script>

        <style>
            /*
            * The path-start class is added to each line
            * to manage its animation - this interpolates
            * between the starting and ending values for the
            * stroke-dashoffset css property
            */
            .path-start {
                -webkit-transition:stroke-dashoffset 5s ease-in;
                -moz-transition:stroke-dashoffset 5s ease-in;
                -o-transition:stroke-dashoffset 5s ease-in;
                transition:stroke-dashoffset 5s ease-in;
            }
        </style>

        <div id='map' class='dark'></div>

        <script>
            L.mapbox.accessToken = 'pk.eyJ1Ijoiam9zaHBlcHBlciIsImEiOiJuTWdrY2k4In0.HCCXtgU04scrTB_-ON4kjA';
            // This is an advanced example that is compatible with
            // modern browsers and IE9+ - the trick it uses is animation
            // of SVG properties, which makes it relatively efficient for
            // the effect produced. That said, the same trick means that the
            // animation is non-geographical - lines interpolate in the same
            // amount of time regardless of trip length.

            // Show the whole world in this first view.
            map = L.mapbox.map('map', 'mapbox.satellite', {zoomControl: false})
                .setView([20, -35], 2);

            // Disable drag and zoom handlers.
            // Making this effect work with zooming and panning
            // would require a different technique with different
            // tradeoffs.
            map.dragging.disable();
            map.touchZoom.disable();
            map.doubleClickZoom.disable();
            map.zoomControl.disable();
            map.scrollWheelZoom.disable();
            if (map.tap) map.tap.disable();

            // Transform the short [lat,lng] format in our
            // data into the {x, y} expected by arc.js.
            function obj(ll) { return { y: ll[0], x: ll[1] }; }

            for (var i = 0; i < pairs.length; i++) {
                // Transform each pair of coordinates into a pretty
                // great circle using the Arc.js plugin, as included above.
                var generator = new arc.GreatCircle(
                    obj(pairs[i][0]),
                    obj(pairs[i][1]));
                var line = generator.Arc(100, { offset: 10 });
                // Leaflet expects [lat,lng] arrays, but a lot of
                // software does the opposite, including arc.js, so
                // we flip here.
                var newLine = L.polyline(line.geometries[0].coords.map(function(c) {
                    return c.reverse();
                }), {
                    color: '#f85c49',
                    weight: 5,
                    opacity: 0.8
                })
                .addTo(map);
                var totalLength = newLine._path.getTotalLength();
                newLine._path.classList.add('path-start');
                // This pair of CSS properties hides the line initially
                // See http://css-tricks.com/svg-line-animation-works/
                // for details on this trick.
                newLine._path.style.strokeDashoffset = totalLength;
                newLine._path.style.strokeDasharray = totalLength;
                // Offset the timeout here: setTimeout makes a function
                // run after a certain number of milliseconds - in this
                // case we want each flight path to be staggered a bit.
                setTimeout((function(path) {
                    return function() {
                        // setting the strokeDashoffset to 0 triggers
                        // the animation.
                        path.style.strokeDashoffset = 0;
                    };
                })(newLine._path), i * 5000);
            }
        </script>
    </body>
</html>