Making a Map

Let’s talk about making a map in D3. D3 is GREAT for making maps and it pretty straightforward to do so.

Let’s create a [choropleth map](https://en.wikipedia.org/wiki/Choropleth_map#:~:text=A choropleth map (from Greek,each area, such as population) for PA.

from IPython.display import HTML, Javascript, display

def configure_d3():
    display(Javascript("""
    require.config({
      paths: {
        d3: "https://d3js.org/d3.v6.min"
      }
    })"""))


configure_d3()

Let’s look at our data

%%html
<div id="output1"></div>
<div id="output2"></div>
<script type="text/javascript">   
require(['d3'], function (d3) {
    
    d3.json("https://gist.githubusercontent.com/dudaspm/89da9e990236d2bc73a3a6ee00c18bb6/raw/055b78a9d016873c36e28872ab6a4d85d0be858e/usCounties.json")
        .then(function(us) {
            d3.select("div#output1").text('{"type":"FeatureCollection", "features": []}')
            d3.select("div#output2").text(JSON.stringify(us["features"][0],null,'\t'))
            
        })
        .catch(function(error){
            console.log(error)
        })
    
})
</script>

Creating the Projection for the US

%%html
<div id="map1"></div>
<script type="text/javascript">   
require(['d3'], function (d3) {
    
    d3.json("https://gist.githubusercontent.com/dudaspm/89da9e990236d2bc73a3a6ee00c18bb6/raw/055b78a9d016873c36e28872ab6a4d85d0be858e/usCounties.json")
        .then(function(us) {
            const width = 800
            const height = 600
            const margin = 0
            // Create the Mercator Projection
            projectionUS = d3.geoMercator().fitExtent([[margin, margin], [width - margin, height - margin]], us)
            // Create a function to generate our paths (counties)
            pathGeneratorUS = d3.geoPath().projection(projectionUS)
            
            const svg = d3.select("div#map1").append("svg")
                .attr("width", width)
                .attr("height", height)
            
            // construct the element
            svg.selectAll("path")
                .data(us.features)
                .join("path")
                .attr('d', pathGeneratorUS)
                .attr('fill', 'none')
                .attr('stroke', '#999999')
                .attr('stroke-width', '2')
            
        })
        .catch(function(error){
            console.log(error)
        })
    
})
</script>

This is a county map for the entire US. Let’s focus on just Pennsylvania. To do this, we need to filter to PA’s data.

If we take a look at the data. The data contains a stateFP for the FIPS for each state.

For PA it is 42.

%%html
<div id="map2"></div>
<script type="text/javascript">   
require(['d3'], function (d3) {
    
    d3.json("https://gist.githubusercontent.com/dudaspm/89da9e990236d2bc73a3a6ee00c18bb6/raw/055b78a9d016873c36e28872ab6a4d85d0be858e/usCounties.json")
        .then(function(us) {
            const width = 800
            const height = 600
            const margin = 0
            // filter data to only PA
            pa = ({type:"FeatureCollection", features:us.features.filter(d=> d.properties.STATEFP==42)})
            
            // Create the Mercator Projection
            projectionPA = d3.geoMercator().fitExtent([[margin, margin], [width - margin, height - margin]], pa)
            // Create a function to generate our paths (counties)
            pathGeneratorPA = d3.geoPath().projection(projectionPA)
            
            const svg = d3.select("div#map2").append("svg")
                .attr("width", width)
                .attr("height", height)
            
            // construct the element
            svg.selectAll("path")
                .data(pa.features)
                .join("path")
                .attr('d', pathGeneratorPA)
                .attr('fill', 'white')
                .attr('stroke', '#999999')
                .attr('stroke-width', '2')
                .append("title")
                .text(d=> JSON.stringify(d.properties,null,'\t'))
                
            
        })
        .catch(function(error){
            console.log(error)
        })
    
})
</script>

Let’s get some data to add to our map.

Example is from Los Angeles Times Data and Graphics Department

An example of using data from the Data Desk’s open-source census-data-downloader

%%html
<div id="output3"></div>
<div id="output4"></div>
<script type="text/javascript">   
require(['d3'], function (d3) {
    
    d3.csv("https://raw.githubusercontent.com/datadesk/census-data-downloader/613e69f6413d917a6db60186e5ddf253e722dcfd/data/processed/acs5_2017_internet_counties.csv")
        .then(function(census) {
            d3.select("div#output4").text(JSON.stringify(census[0],null,'\t'))
            
        })
        .catch(function(error){
            console.log(error)
        })
    
})
</script>

So, based on our data we have a state and a county code (like above). Let’s use this data to see the percentage of internet access per county in PA.

%%html
<div id="map3"></div>
<div id="legend1"></div>
<script type="text/javascript">   
require(['d3'], function (d3) {
    
    d3.json("https://gist.githubusercontent.com/dudaspm/89da9e990236d2bc73a3a6ee00c18bb6/raw/055b78a9d016873c36e28872ab6a4d85d0be858e/usCounties.json")
        .then(function(us) {
        d3.csv("https://raw.githubusercontent.com/datadesk/census-data-downloader/613e69f6413d917a6db60186e5ddf253e722dcfd/data/processed/acs5_2017_internet_counties.csv")
            .then(function(census) {
                const width = 800
                const height = 600
                const margin = 0
                // filter data to only PA
                pa = ({type:"FeatureCollection", features:us.features.filter(d=> d.properties.STATEFP==42)})

                // Adding our census data
                census = census.filter(d=> d.state==42)
                censusObject = {}
                census.forEach(d=> censusObject[d.county] = (+d.total_no_internet/+d.universe))
                
                // create our color
                palette = d3.interpolatePurples
                color = d3.scaleLinear().range([0,1]).domain(d3.extent(census,d=> (+d.total_no_internet/+d.universe)))
                
                // Create the Mercator Projection
                projectionPA = d3.geoMercator().fitExtent([[margin, margin], [width - margin, height - margin]], pa)
                // Create a function to generate our paths (counties)
                pathGeneratorPA = d3.geoPath().projection(projectionPA)

                const svg = d3.select("div#map3").append("svg")
                    .attr("width", width)
                    .attr("height", height)

                console.log(censusObject)
                // construct the element
                svg.selectAll("path").append('path')
                    .data(pa.features)
                    .join("path")
                    .attr('d', pathGeneratorPA)
                    .attr('fill', d=> palette(color(censusObject[d.properties.COUNTYFP])))
                    .attr('stroke', '#999999')
                    .attr('stroke-width', '2')
                    .append("title")
                    .text(d=> "Location: "+d.properties.NAME+"\nData: "+censusObject[d.properties.COUNTYFP])

            })
            .catch(function(error){
                console.log(error)
            })

                
            
        })
        .catch(function(error){
            console.log(error)
        })
    
})
</script>
%%html

<script type="text/javascript">   
require(['d3'], function (d3) {
    
    d3.csv("https://raw.githubusercontent.com/datadesk/census-data-downloader/613e69f6413d917a6db60186e5ddf253e722dcfd/data/processed/acs5_2017_internet_counties.csv")
        .then(function(census) {
            const width = 800
            const height = 100
            const margin = 40 
            census = census.filter(d=> d.state==42)
            palette = d3.interpolatePurples
            x = d3.scaleLinear().range([margin,width-margin]).domain(d3.extent(census,d=> (+d.total_no_internet/+d.universe)))
            
            const svg = d3.select("div#legend1").append("svg")
                .attr("width", width)
                .attr("height", height)
            
            
            const xAxis = d3.axisBottom().scale(x)
            
            svg.append("g")
                .attr("class", "axis")
                .attr("transform", "translate(0," + (height-margin) + ")")
                .call(xAxis) 

            svg.append("text")
                .attr("x", width/2)
                .attr("y", height-5)
                .style("text-anchor", "middle")
                .text("No Internet/All Data (%)")
                       
            
            const num = 20
            const values = d3.range(1,num)
            
            const coloring = d3.scaleLinear().range([0,1]).domain(d3.extent(values))
            var defs = svg.append("defs")
            var linearGradient = defs.append("linearGradient")
                .attr("id", "linear-gradient1") 

            linearGradient.selectAll("stop").data(values).join("stop")
                .attr("offset", d=> d/num)
                .attr("stop-color", d=>palette(coloring(d)) )
            svg.append("rect")
                .attr("x", margin)
                .attr("y", (height-margin)-50)
                .attr("width", (width-margin)-(margin))
                .attr("height", 50)
                .style("fill", "url(#linear-gradient1)")

            
        })
        .catch(function(error){
            console.log(error)
        })
    
})
</script>