Reading Notes for D3.js Part VIII (Chapter 12)

  |   Source

This post is transferred from my old Hexo blog site, created on 2014.

This is a Reading Note for Interactive Data Visualization for the Web - An Introduction to Designing with D3 by Scott Murray, pulished by O'Reilly, 2013

This post will contains concepts that I think it's important for me to be familiar with in D3 book.

Chapter 12 : Map

code example for Chapter 12 is here.

GeoJSON is a special format for JSON, which is used specially for geography. A typical GeoJSON file is an object with two attributes: type and features. The features attibute is an array of objects. Each objects have geography attributes, such as type,id,properties,geometry.

geometry is a vary useful attributes. It can include coordinate attribue, which inludes longitude,latitude pairs. Note longitude is vertical on map, latitude is horizontal on map. Here is a sample: images/posts/sampleGeoJSON.png

Step 1: transfer GeoJSON to SVG path code by using d3.geo.path(). Then load GeoJSON file and apply new path:

//set a path generator
var path = d3.geo.path();

//load map json file. Since this is a callback function, code outside will run the same time as inside
d3.json('/js/us-states.json',function(json){
    svg.selectAll('path')
        .data(json.features)
        .enter()
        .append('path')
        .attr('d',path); //This line will create a path generator to get all geography data and generate svg path
});

Step 2: create projection to transfer 3D to 2D, then use d3.geo.path().projection() to apply projection to path. You can also add scale() to make map smaller or larger:

//Apply scale and Use projection to let map cover all states in US, using a build-in projector albersUsa()
var projection = d3.geo.albersUsa()
                        .translate([w/2,h/2])//Make projection to the center of svg
                        .scale([500])//make map smaller. Default value is 1000.
var path = d3.geo.path().projection(projection); //Apply new projection to current path;

Use Color to Quantize Map

We can add color with different combination to map to show data difference in map. For example, the place that has more populatio with dark blue and less population with light green, etc.

Step1: create a quantize scale with color as output. A combination of color can be found in https://github.com/mbostock/d3/tree/master/lib/colorbrewer, from D3 library. Now we output discreate value as color. The number of colors decides how precise the map will be. Less color group will make map has less color combination:

var color = d3.scale.quantize()
                .range('['rgb(237,248,251)','rgb(204,236,230)','rgb(153,216,201)','rgb(102,194,164)','rgb(44,162,95)','rgb(0,109,44)']');
    //Here we want to devide data to different bucket.

Step2: use d3.json() or d3.csv() to load data. In data callback function, set up color input domain:

d3.json('sample.json',function(data){ //note we use var data to represent data in json file
    color.range(
    [d3.min(data, function(d){return d.value}), d3.max(data, function(d){return d.value;})]
    );
    //Here we get min and max value from data
});

Step3: load data. We may need to combine data in two json/csv file. To do this, wrap another data callback function in current data callback function, then compare column with same data. if it fits, add an attribute in a dataset with the nessiary data from the other dataset. You can use nested for loop to compare data:

d3.json('1.json',function(data1){

    d3.csv('2.csv',function(data2){ //load another csv file

    for(var i=0; i<data1.attr1.length; i++){ //First for loop for data in json
        var combine1 = data1.attr1[i].data/value/...;

        for(var j=0; j<data2.attr2.length; j++){ //Second for loop for data in csv
            var combine2 = data2.attr2[i].data/value/...;
            if(combine1 == combine2){ //Check if combine 1 and combine 2 has the same data that can be joined
                combine2.attr2[i].newValue = data1.attr1[i].value;
                //Get the data we need from data1 and put it as a new attribute for data2
                break;
                //After find a proper fit, jump out of current for loop and check another value from data1
            }
        }
    }

    })
})

Step 4: create path and load data to path. You need to do this inside a callback loop since we need to use data variable in loop. We need to check if the value in second json/csv exist or not in first json/csv. If not, we need return a single color:

svg.selectAll('path')
    .data(data1.attr1)
    .enter()
    .append('path')
    .attr('d','path')
    .style('fill',function(d){
        var value = d.attr1.value; //value that you want to apply color

        if(value) //check exist
        {
            return color(value);
        }else{
            return "grey";
            //if no value, make this part of map as grey.
        }
    })

Step 5: create and add projection. Note you need to do this step outside of callback function so that your result will not be affected.

Add Circle as Data Point on Map

If we have json/csv file that have city names, value, and cities longitude, latitude data, we can make it on map.

Step1, Step2, Step3, Step4: Same as example above.

Step 5: load new data and add circle to map. You need create projection function before this step, buy don't apply it to all map. Also, you need to put this part inside callback function, so that your circle will not be covered by map since map will be loaded later than outside functions:

svg.csv('3.csv',function(data3){
    svg.selectAll('circle')
        .data(data3)
        .enter()
        .append('circle')
        .attr('cx', function(d){
        return projector([d.lon,d.lat])[0];
        //Apply your projector function to the array combined by lon and lat, then get first element as new lon

        }).attr('cy', function(d){
        return projector([d.lon,d.lat])[1];
        //Apply your projector function to the array combined by lon and lat, then get first element as new lat

        }).attr('r',functon(d){
            return Math.sqrt(parseInt(d.attrName)*0.00004);
            //Get the value of the attribute value you want to use, then apply Math.sqrt to get r from coverage size.
        }).attr('fill','yellow')
        .attr('opacity',0.75);
        //You can set up opacity if you want
})

Code For This Chapter

code example for Chapter 12 is here.

//Part I Code
function part1(){
//create svg
var h = 500;
var padding = 5;
var svg = d3.select(document.getElementById('chapter12').getElementsByClassName('1')[0]).append('svg').attr('width','100%').attr('height',h);
var w = document.getElementsByTagName('svg')[0].offsetWidth;

//set a path generator
var path = d3.geo.path();

//load map json file. Since this is a callback function, code outside will run the same time as inside
d3.json('/data/us-states.json',function(json){
    svg.selectAll('path')
        .data(json.features)
        .enter()
        .append('path')
        .attr('d',path); //This line will create a path generator to get all geography data and generate svg path
});

//Apply scale and Use projection to let map cover all states in US, using a build-in projector albersUsa()
var projection = d3.geo.albersUsa()
                        .translate([w/2,h/2])//Make projection to the center of svg
                        .scale([500])//make map smaller. Default value is 1000.
var path = d3.geo.path().projection(projection); //Apply new projection to current path;
}


//Part II Code
function part2(){
//Create svg
var h = 500;
var padding = 5;
var svg = d3.select(document.getElementById('chapter12').getElementsByClassName('2')[0]).append('svg').attr('width','100%').attr('height',h);
var w = document.getElementsByTagName('svg')[0].offsetWidth;


//Create a scale with different color for quantize map. Here we devide color to 6 groups. You can set your own.
//Color reference: https://github.com/mbostock/d3/tree/master/lib/colorbrewer
var colorBrew = ['rgb(237,248,251)','rgb(204,236,230)','rgb(153,216,201)','rgb(102,194,164)','rgb(44,162,95)','rgb(0,109,44)'];
var color = d3.scale.quantize()
                .range(colorBrew); //We need to wait till data finish loading to add input domain


//Set up path generator
var path = d3.geo.path();


//Load data
d3.csv('/data/us-ag-productivity-2004.csv',function(data){
    color.domain([
            //set up input domain after data has been loaded
            d3.min(data,function(d){return d.value;}),
            d3.max(data,function(d){return d.value;})
        ]);

    //Add csv data to json because we can only bind one group of data to elements.
    //Note you need to do this in d3.csv() because this step rely on csv data
    d3.json('/data/us-states.json',function(json){

        //Mix data in csv file
        for(var i=0; i<data.length; i++){
            var dataState = data[i].state; //get state name in csv file
            var dataValue = parseFloat(data[i].value); //get value in csv file. Note the column of csv file is state and value

            for(var j=0; j<json.features.length; j++){
                var jsonState = json.features[j].properties.name; //Find state name in json file

                if(jsonState == dataState){
                    //copy csv value to json file, make a key as value
                    json.features[j].properties.value = dataValue;
                    //go out for loop. we don't need to check more state in json file.
                    break;
                }
            }
        }

        //Now create path and apply data
        svg.selectAll('path')
            .data(json.features)
            .enter()
            .append('path')
            .attr('d', path)
            .style('fill',function(d){
                var value = d.properties.value;
                if(value){ //check if value exist or not
                    return color(value);
                }else{
                    return "grey";
                }
            });
        });
});

//Add Projector
var projection = d3.geo.albersUsa()
                        .translate([w/2,h/2])//Make projection to the center of svg
                        .scale([500])//make map smaller. Default value is 1000.
path.projection(projection);
}


//Part III Code
function part3(){
    //create svg
    var h = 500;
    var padding = 5;
    var svg = d3.select(document.getElementById('chapter12').getElementsByClassName('3')[0]).append('svg').attr('width','100%').attr('height',h);
    var w = document.getElementsByTagName('svg')[0].offsetWidth;

    //create projector
    var proj = d3.geo.albersUsa().translate([w/2,h/2]).scale([500]);

    //create path generator
    var path = d3.geo.path();

    //create color brew
    var colorBrew = ["#ffffd4","#fed98e","#fe9929","#d95f0e","#993404"]; //This time use 5 groups. Color effect will be different from example 2
    var color = d3.scale.quantize().range(colorBrew);

    //load population change.csv file
    d3.csv('/data/us-ag-productivity-2004.csv',function(data){

            //set color input domain
            color.domain(
                [d3.min(data, function(d){return d.value;}),
                 d3.max(data, function(d){return d.value;})]
                );

            //Combine data in csv to data in json
            d3.json('/data/us-states.json', function(data2){
                for(var i=0; i<data.length; i++){
                    var dataState = data[i].state;
                    var dataValue = parseFloat(data[i].value);

                    for(var j=0; j<data2.features.length; j++){
                    var data2State = data2.features[j].properties.name;
                    if(dataState == data2State){
                        data2.features[j].properties.value = dataValue;
                        break;
                    }
                    }
                }



                //Draw map and add color,projector
            svg.selectAll('path').data(data2.features).enter().append('path')
                .attr('d',path).style('fill',function(d){
                var value = d.properties.value; //note since we use data2.features as dataset, here d is equal to data2.features single item
                if(value){
                    return color(value);
                }else{
                    return "black";
                }
            });

            //load csv data from us cities.csv. Make sure you do this in data callback function so that circle is on top of map. Otherwise circle will be covered by map
            d3.csv('/data/us cities.csv',function(data3){
                //create circle that represetns city, then apply projector to it
                svg.selectAll('circle').data(data3).enter()
                    .append('circle')
                    //ImportantL proj(lon,lat) will return a array of two
                    .attr('cx',function(d){
                        return proj([d.lon, d.lat])[0]; //0 postion is longitude
                    }).attr('cy',function(d){
                        return proj([d.lon, d.lat])[1]; //1 position is latitude.
                    }).attr('r',function(d){
                        return Math.sqrt(parseInt(d.population) * 0.00004); //use sqrt to get r from coverage of population
                    }).attr('fill','green')
                    .attr('opacity',0.75);
            });
            });
    });

            //apply projector
            path.projection(proj);
            //Important! When you use projection for full map(i.e, all paths), you must do this out side data callback function!


    }
Comments powered by Disqus