Lab 7. Time Series Proportional Symbols

Due: 11:59 pm, Thursday, 11/19

    Overview

    In this lab, we will create a time series proportional symbol map with Leaflet and JQuery (another JS library). This lab exercise is modified from this tutorial.

    The scripts contain a lot of function definitions and may be difficult to follow if you are trying to understand them line by line. I have provided comments as explanations. However, you may use the scripts as a template to map data with a similar structure. I am always happy to explain in more details if you are interested in learning the syntax.

    Please read the instructions carefully and complete the assignment in the Deliverables section at the bottom. And you will need to apply some of the data processing skills introduced in Lab 6, e.g., table join, polygon centroid, to put the deliverable data together.

    *******************************************************************************************************************

    Prepare Time Series Data

    The first step for the spatiotemporal visualization is to create a time series dataset. As an example, I'll map population dynamics of fifteen major U.S. cities. Below is an example of what the dataset looks like (a .csv file):

    • Click here to download the example data. Spend some time to make sure you understand the data structure.
      For the deliverables, you will create a time series tabular data of Zika outbreak in the U.S. from separate data files (link provided). Note the timestamp columns should be placed in chronological order from left to right.
    • In the previous lab, we introduced how to display a csv table with coordinate information as points in QGIS. We are introducing an online tool this time, which may come in handy when other GIS processing (e.g., table join, simplify geometry) is not required.
    • In your web browser, go to geojson.io, select Open - File (see image below) to open the csv file you have just downloaded.
    • The data, i.e., 15 U.S. cities, should have been loaded as points/markers to the site.
    • Next, click Save - GeoJSON to save the data as a geojson file to your lab 7 folder.
    • Since we use a JQuery function to read/parse the data this time, we can keep the .geojson file for mapping - there is NO need to save it as a .js file.

    Proportional Symbol Map

    We will map the city population data as proportional symbols. Not sure what is a proportional symbol map? - click here to learn more.

    • First, open your code editor and set up the basemap to show the contiguous U.S. in fullscreen mode, and save the document as map7.html to your lab 7 folder, where the geojson file was saved.
    • Next, reference the JQuery libraries so that we could use the JQuery functions (add to the head section):
      <script src="https://code.jquery.com/jquery-3.5.1.min.js"
      integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
      crossorigin="anonymous"></script>
      
      <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"
      integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU="
      crossorigin="anonymous"></script>
    • This time, we use a JQuery function getJSON() to load the GeoJSON data directly. However, this function requires a hosted absolute url of the geojson file. To do this:
      • Upload the geojson file to GitHub first - start a new repository where you will publish your map.
      • Next, click the geojson file in the repository and select Raw to copy the hosted link of the geojson file (see the demonstration below). Note that only the raw link to the geojson file works as an absolute url.
    • To use the getJSON() function, a $ sign is used to define/access jQuery. Place the following lines AFTER adding the tile layer. Note you may need to change the url of the geojson file to match yours:
      //change the file url to match yours
      $.getJSON("https://raw.githubusercontent.com/neiugis/lab7_map/main/city.geojson")  // The getJSON() method is used to get JSON data
      .done(function(data) {
      
      });
    • The data doesn't show up on the map at this point. We will define how to draw the data in the following steps.
    • We first define a function to process the data to acquire the timestamps (the year columns) and the maximum and minimum population values to be used for creating the legend (extra credit question).
      Add the function below (you may name the function differently) following the loading data portion:
      function processData(data) {
        // First, initialize the variables to hold the timestamps and min/max population values
        var timestamps = [];  // square brackets to define an array of data
                              // because there are multiple timestamps
        var	min = Infinity; // for the min, begin with the largest possible value - infinity
        var	max = -Infinity;// for the max, begin with the smallest possible value - negative infinity
      
        // Go through each row/feature of the data table
        // Note data is the variable name in the function definition - processData(data)
        for (var feature in data.features) {
            var properties = data.features[feature].properties;
      
            // At each row, go through the columns/attributes to get the values
            for (var attribute in properties) {
                if ( attribute != 'id' &&
                     attribute != 'name' &&
                     attribute != 'latitude' &&
                     attribute != 'longitude' )   // != means NOT EQUAL TO
                                            // These columns are NOT recorded
                                            // Modify this part when mapping your own data
                {
                    if ( $.inArray(attribute,timestamps) ===  -1) { // JQuery in.Array() method searches for a specified value within an array and return its index (or -1 if not found)
                                                      // here, the new timestamp is only added when it is not already in the array
                                                      // triple equals === compares both type and value
      
                        timestamps.push(attribute);  // The JS push() method adds new items to the end of an array
                                                     // and returns the new length of the array
                    }
                    if (properties[attribute] < min) {
                        min = properties[attribute]; // record/update the current smaller values as the min
                    }
                    if (properties[attribute] > max) {
                        max = properties[attribute]; // record/update the current larger values as the max
                    }
                }
            }
        }
        return { // the function finally returns the timestamps array, the min and max of population data
            timestamps : timestamps,
            min : min,
            max : max
        }
      }
    • Read the comment lines to learn more about what this function does exactly. It looks like a lot going on. But when you map your own data, the only places you need to change are the attribute/column names that are NOT supposed to be mapped ('id', 'name', 'latitude', 'longitude' in this case).
    • Next, add the functions below to draw proportional symbols (following the processing data portion):
      // The function to draw the proportional symbols
      function createPropSymbols(timestamps, data) {
      
        cities = L.geoJson(data, {
      
            // By default, Leaflet draws geojson points as simple markers
            // To alter this, the pointToLayer function needs to be used
            pointToLayer: function(feature, latlng) {
                return L.circleMarker(latlng, { // we use circle marker for the points
                    fillColor: "#501e65",  // fill color of the circles
                    color: '#501e65',      // border color of the circles
                    weight: 2,             // circle line weight in pixels
                    fillOpacity: 0.5       // fill opacity (0-1)
                }).on({
                      mouseover: function(e) {
                          this.openPopup();
                          this.setStyle({fillColor: 'green'});  // fill color turns green when mouseover
                      },
                      mouseout: function(e) {
                          this.closePopup();
                          this.setStyle({fillColor: '#501e65'});  // fill turns original color when mouseout
                      }
              });
            }
        }).addTo(map);
      
        updatePropSymbols(timestamps[0]); // this function is defined below
                                        // When loaded, the map will first show proportional symbols with the first timestamp's data
      }
      
      // The function to update/resize each circle marker according to a value in the time series
      function updatePropSymbols(timestamp) {
      
        cities.eachLayer(function(layer) {  // eachLayer() is an Leaflet function to iterate over the layers/points of the map
      
            var props = layer.feature.properties;   // attributes
            var radius = calcPropRadius(props[timestamp]); // circle radius, calculation function defined below
      
            // pop-up information (when mouseover) for each city is also defined here
            var popupContent = props.name + ' ' + timestamp + ' population: ' + String(props[timestamp]) ;
      
            layer.setRadius(radius);  // Leaflet method for setting the radius of a circle
            layer.bindPopup(popupContent, { offset: new L.Point(0,-radius) }); // bind the popup content, with an offset
        });
      }
      
      // calculate the radius of the proportional symbols based on area
      function calcPropRadius(attributeValue) {
      
        var scaleFactor = 0.001;   // the scale factor is used to scale the values; the units of the radius are in meters
                                   // you may determine the scale factor accordingly based on the range of the values and the mapping scale
        var area = attributeValue * scaleFactor;
      
        return Math.sqrt(area/Math.PI);  // the function return the radius of the circle to be used in the updatePropSymbols()
      }
      
      Again, a lot of things going on here. But you ONLY need to adjust the popup content (as we did before) and the scale factor (depending on the value and map scale) for making your own maps. And of course, customize the colors and styles of the circles as you like.
    • Next, we will call the defined functions in the loading data portion to apply them. MODIFY the getJSON function to add the data processing and symbol drawing functions we just defined:
      $.getJSON("https://raw.githubusercontent.com/neiugis/lab7_map/main/city.geojson")
      .done(function(data) {
          var info = processData(data);
          createPropSymbols(info.timestamps, data);
      });
    • Your map should be displayed as proportional symbols with the first timestamp's data at this point (sample code):

    Add a Time Slider

    Now we will add the time slider, with labels, to animate the temporal data. For this lab, we will use the HTML5 range type to create a simple slider.

    • Add the functions below AFTER the drawing symbol portion:
      function createSliderUI(timestamps) {
        var sliderControl = L.control({ position: 'bottomleft'} ); // position of the slider
                          // Another use of L.control :)
        sliderControl.onAdd = function(map) {
          //initialize a range slider with mousedown control
            var slider = L.DomUtil.create("input", "range-slider");
            L.DomEvent.addListener(slider, 'mousedown', function(e) {
                L.DomEvent.stopPropagation(e);
            });
      
          // Define the labels of the time slider as an array of strings
          // Modify this for your data
          var labels = ["1950", "1960","1970","1980", "1990", "2000","2010"];
      
          $(slider)
              .attr({
                'type':'range',
                'max': timestamps[timestamps.length-1],
                'min':timestamps[0],
                'step': 10, // Change this to match the numeric interval between adjacent timestamps
                'value': String(timestamps[0])
              })
              .on('input change', function() {
                  updatePropSymbols($(this).val().toString()); // automatic update the map for the timestamp
                  var i = $.inArray(this.value,timestamps);
                  $(".temporal-legend").text(labels[i]); // automatic update the label for the timestamp
              });
          return slider;
        }
        sliderControl.addTo(map);
        createTimeLabel("1950"); //The starting timestamp label
        }
      
      
        // Add labels to the time slider when the map first loaded
        function createTimeLabel(startTimestamp) {
          var temporalLegend = L.control({position: 'bottomleft' }); // same position as the slider
                             // One more use of L.control !!
          temporalLegend.onAdd = function(map) {
            var output = L.DomUtil.create("output", "temporal-legend");
            $(output).text(startTimestamp);
            return output;
          }
          temporalLegend.addTo(map);
        }
    • For your own data, you will need to change the step value to match the interval between adjacent timestamps (10 in this example), and also change the label texts.
    • Then, MODIFY the getJSON function to add the slider creation function:
      $.getJSON("https://raw.githubusercontent.com/neiugis/lab7_map/main/city.geojson")
      .done(function(data) {
          var info = processData(data);
          createPropSymbols(info.timestamps, data);
          createSliderUI(info.timestamps);
      });
    • The simple slider is added to the map, at your defined position (I used bottom left) (sample code):

    Deliverables

    For the deliverables, we use the Zika outbreak data. Click here to access the data files. The folder contains 11 csv files with monthly zika case counts at state level (including the territories), in 2016. Each file only contains states with zika cases at the corresponding timestamp.

    • Choose AT LEAST five files to download (you could read the timestamp information in the file name).
    • Create a time series data table according to the example data (Hint: it might be easier to begin with data from a later time).
      • Consider using QGIS - Table Join to combine the separate tables into one. Let's use the travel associated cases here, which would be more interesting to map.
      • The timestamp columns should be placed in chronological order, and you may want to rename the timestamp columns so that the numeric interval between adjacent timestamps is a constant, e.g., 1, 2, 3, 4, 5.
      • Please make sure to assign NULL values when a state does not have a case (you may export the joined table (create CSVT when saving as) and do this in Excel). Using NULL value is to ensure the no-case states won't be displayed. Otherwise, you will see a small circle at the center of each state.
      • Also the table doesn't contain coordinate information of the states. Recall how we generated coordinates for counties last time. You may download the state boundary file here, generate centroids, save in WGS84, calculate lat and lon, then table join the coordinates to the csv.. (I know it is a lot but it is also important to review as you will likely need to do this often with real data.)
      • You may find there are a few states/territories with NULL lat/lon (Pennsylvania, American Samoa, and U.S. Virgin Islands), as the census boundary file doesn't include the unincorporated territories and there was some random characters added to Pennsylvania in the csv file. You may manually input the lat and lon for Pennsylvania and delete American Samoa, and Virgin Islands in your final joined/exported csv before saving it as a GeoJSON file (either with geojson.io or QGIS).
      • ASK ME right away if in doubt!
    • Create a time series proportional symbol map of the zika outbreak, add a map title with L.control (review Lab 6 if needed).
    • Your final map will be graded based on its overall design and accuracy, e.g., proportional symbol size, timestamps and labels, pop up content, etc.
    • Extra Credits: Create a map legend for the proportional symbols (Hint: You may visit this tutorial and see Step 8 to learn how;
      Note if you copy the code from the tutorial, you may need to fix the curly quotes to straight quotes to make it work.)
    • Host your map via GitHub (Make sure to rename the html file as index.html and upload any associated data files to the repository!!!) and submit the url through D2L.