As usual, go to Github Classroom and grab the starter code for the lab.
We are going to do a simple visualization to learn something about the location of major supermarket chains in the UK (why you ask? well, why not? besides you've already seen how to make the US map). I have gathered together a list of the supermarket locations from the six major chains in the UK (about 2900 stores). We will look at these in a couple of different ways. Mainly we will be looking at the density of the supermarkets as distance to the closest supermarket can indicate population density, among other things.
If you look in the repository, you will see that I've given you the data this time. I just want to make sure everyone gets a chance to see this in action.
Step one Navigate to the directory holding the lab files in the terminal. (We are starting off a server that can serve file off your computer -- best to limit its scope as tightly as possible...)
Step two Locate python as needed. You all had to install python version 3 at the start of the class, so if you want, you can use that one (conda activate infovis). If you have a different python version 3 in your path, you are welcome to use that instead. (All Mac users have python version 2 available, but the instructions are slightly different - -see Piazza).
Step three Start the server. Type: python -m http.server. You should see something like this:
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Step four Open the provided URL in your browser. You should see the directory contents. Click on the main file.
As a side note, it would not be a bad idea to copy my data files to your local repositories for all of your assignments now that you know how to handle local data files. That way, when I archive the course website, all of your code will still work...
Look at the starter code -- you should see that double load that I showed you in the other map examples. Print out map_data and supermarket_data (or use a breakpoint and examine the contents that way).
The map data is from a TopoJSON file. TopoJSON is a an alternate file format to GeoJSON. The main difference between TopoJSON and GeoJSON is their handling of shared edges. In GeoJSON, all shapes are atomic. If you think about our US map, this means that there is a large amount of redundancy as many state borders are nothing but shared edges. TopoJSON starts by defining a collection of edges("arcs"), and then listing out which edges make up the individual shapes.
You can see this inside of the structure -- there is a list of arcs, followed by a list of objects. There are two kinds of object: uk_countries and uk_places. The later is mostly city locations, which we will ignore for the time being. Open uk_countries, and you will see the geometries. There is one for each country (yes, Ireland is in the list despite not being part of the UK -- the map looks funny if we leave it off...). Each country has a list of arcs that make up its outline. Mostly, you won't have to fully understand the structure to use it, but it is helpful to have a rough idea of what is in there.
The supermarket_data is much more straightforward -- just the store name and the longitude and latitude.
Before we draw the map, we have to set up the projection function. In the examples I provided, we used the d3.geoAlbersUsa projection. As should be obvious from the name, this one is an Albers projection that has been tuned to the US (complete with the boxed Alaska and Hawaii). For the UK, we need to use a more generic projection (in this case, still an Alber's projection), which means we have to pay more attention when we set it up.
Step one Create the projection. Create a projection constant and assign it d3,geoAlbers(). Now we will do a little method chaining to configure it.
Step two Set the center of the projection. This is the longitude and the latitude at the center of the projection. We will use [0, 55.4]. If you check this in Google Maps (enter "55.4,0" into the search), you will see this is out in the North Sea. Call the center function and pass it [0, 55.4].
Step three Rotate the globe. We are now going to rotate the globe a little to center ourselves. Call rotate with [4.4,0]. This gives us an effective center of 4.4W 55.4N (if you check this in Google Maps, you can see we are now centered in southern Scotland).
Step four Scale up. Our map will be very tiny now, so we need to blow it up a bit. Use the scale function to set the scale to 3500.
Step five Move the projection to the center of the canvas. Use the translate function to move the projections to width/2, height/2. Note that this is actually translate, not an attribute called transform and value of "translate".
Step six Create the path tool. The path tool is responsible for translating our GeoJSON data into path descriptions (like our line function). We will use the same path tool we used in the earlier examples: const path = d3.geoPath().projection(projection);
While TopoJSON is great for moving data around, when it is time to draw, we need the full outline of our shapes. So, we are going to convert the data we want to map to GeoJSON format. If you look at the header of the HTML file, you will see I snuck a new library in there for handling TopoJSON. We are going to use the topojson.feature() function to do the conversion for us.
The command you want is this one: const uk_countries = topojson.feature(map_data, map_data.objects.uk_countries).features;
Note how it is extracting the data just for uk_countries object that we saw earlier in the data. This will be different for different data sources. The final .features is extracting just the features from the GeoJSON object.
Finally, we can actually draw the map. We are going to draw in layers, so the first thing we will do is append a "g" onto the SVG, then we will do our drawing on there.
Step one Append a "g" to svg. Give it an id of "map-layer".
Step two Add the paths. Do the normal selectAll, data, enter, and append dance. Your data will be uk_countries. For the "d" attribute, you can just pass in the path function.
Step three Style the paths. Try a fill of "lightblue" and a stroke of "black". You can experiment with different colors later.
Step four Try it out. Load the page -- you should see a map of the UK (and Ireland).
We will represent each supermarket with a dot.
Step one Create a new layer. Add a new "g" to the svg. Give this one an id of "point-layer".
Step two Add the dots. Again, do the familiar data dance. This time create circles using supermarket_data. "cx" should be set to (d) => projection([+d.longitude, +d.latitude])[0]. This runs the longitude and latitude of the store through our projection to give us pixel coordinates. Recall that the projection returns a point in 2D space, so we have to extract the first value for "cx". "cy" looks exactly the same, but this time you want the second value in the returned array. Give your dots a radius of 2, set the fill to "black" and set the opacity to .5.
Step three Try it out. You should see a lot of dots. London, in particular, stands out in southern England.
You will hopefully have noticed the collection of checkboxes and the function at the bottom that handles when they are clicked. Toggling them currently just prints out to the console. I want you to selectively hide the layers when their checkbox is toggled.
Step one Hide the layer. I have added a "hidden" class. Use classed to give the point layer this class (make sure it is the "g", not the points themselves).
Step two Use classed to toggle the visibility in the event handler. If you look at the console logs, I've shown you how to get the value of the checkbox which changed (which should also be the id of the corresponding layer), and the current state of the checkbox. Note that the logic is flipped: when the box is checked, we don't want the layer to be a member of the class "hidden".
Step four Test and make sure the dots come and go.
We can see that the stores clearly cluster in certain locations around the UK. To get a better sense of the clustering, we will make a contour density plot. We will figure out the supermarket density across the UK, and identify regions of different densities. Fortunately, we don't need to do any of the hard work ourselves -- this is a facility built into D3 in the form of d3.contourDensity.
Basically, all we need to do is toss all of our point data into the density estimator, and it will output a collection of contour shapes.
Step one Create the contour density function. This follows the same pattern we have seen many times: call d3.contourDensity() and it will return the function. Save this in a variable called density_function (this will be the function we call on our data). Of course, we need to configure it first...
Step two Provide the x and y accessors using x() and y(). These are just like the accessors we used when making the lines -- they just tell the function how to extract x and y from the data elements. We will pass the estimator our supermarket data, so we need to pass it the longitude and latitude data. To make our lives easier, we will work in pixel space, so we want to pass the projected point. Use the same projection syntax that you used to draw the points.
Step three Set the size for the estimator. We need to tell the estimator The bounds in which our data lies. Since we are working in pixel space, we can just pass width and height. Call the size function, passing it the array [width height].
Step four Set the standard deviation. This gives us a measure of control over the tightness of the contours. We set it with the bandwidth function. By default this is about 20.5, but I found 15 to be a good value.
Step five Call your new estimator on supermarket_data. Call the function you created (density_function) and pass it the supermarket_data. Save the result in a variable called density_data.
Step siz Make the contour layer. We will follow the same pattern as we did for the points. Append a new "g" to the svg and give it the name contour-layer. Note that the check box for it should already function, so it will be hidden initially.
Step seven Add the contours. Break out the selectAll, data, enter, and append sequence again, and create paths bound to density_data. Set the fill to "none", and the stroke to "black". For the "d" attribute, we need to pass it a geoPath function, but we have already performed the projection, so we don't want to use our existing path function. The solution is to just pass it a new geoPath without an associated projection: d3.geoPath(). It is important to call the function this time, as this is the generator function that will return the function we want to use.
Step eight Try it out. You should see some nice contour lines when you select the "Contours" check box.
The contours are fine, but a nice colored isopleth map would look nice. We are actually going to use exactly the same contour items. However, instead of drawing the outlines, we will turn off the stroke, and set the fill. By making each layer partially transparent, we will see different colors as the layers stack.
Step one Copy the code that makes the contours. Really -- just copy it. You don't need the part that creates the density function and data, just the drawing part.
Step two Change the name. Change the id from "contour-layer" to "isopleth-layer". Do this to the first of the two blocks so that the contour lines can be drawn on top.
Step three Change the styling. Set the stroke to "none", and the fill to "mediumblue". Then set the opacity to .2.
Step four Try it.
We have a pretty good map at this point, and we could be happy with it as it is... but I'm not. I don't like that the contours go out into the ocean. Fortunately, SVG supports clipping to arbitrary shapes, so we can use the countries to clip the contours.
Step one Create the clip path. We will add this to the SVG before we do anything. Just append a "clipPath" element to the SVG. Give it the id "map-clip".
Step two Tell the clip path what to clip. We could get fancy to avoid duplication, but we will take a easy way out. Just copy the code that draws your map and paste it in here. In other words, after setting the id, do a selectAll("path"), bind it to the uk_countries data, do the enter() and append() thing, and set the "d" attribute. We only need the shape -- we don't need the style, class or id information.
Step three Tell the isopleth and contour plot to use the new clip path. Set the attribute "clip-path" and pass it "url(#map-clip)". This looks up the element of that id on the page and uses it to clip to.
Step four test it out. The contour and isopleth plots should now nicely clip the the bounds of the country.
Feel free to experiment (or not) to make the map your own.