Search code examples
javascriptd3.jstopojson

D3 Map + Graph Coordinated Visualization: selecting wrong countries / map changing attributes


Here is my issue: I have a map SVG and a bar Graph SVG. I want to create a coordinated selection. For instance, the user highlights a bar in the graph and the country on the map corresponding to that data is also highlighted and Vice Versa.

Right now I can highlight any country on the map and the proper corresponding bar will also highlight. However, the same does not work for the bars. When I highlight a bar, a random country is highlighted and after that the country names, as displayed in a tooltip, are all jumbled and wrong.

Here is the map -> bar graph highlight:

...
           map.selectAll("countries")
                .data(b.features)
                .enter()
                .append("path")
                .attr("d", path)
                //.style("stroke", "black")
                .on("mouseover", function(d) { 
                    activeDistrict = d.properties.ADMIN,
                    chart.selectAll("rect")
                    .each(function(d) {
                        if(d){

                            if (d.Country == activeDistrict){
                                console.log("confirmed" + d.Country)
                                d3.select(this).style("stroke", "blue").style("stroke-width", "3");

                            }
                        }
                    })

...

Here is the bar graph -> map highlight. This is the function I cannot get to behave properly.

            var bars = chart.selectAll(".bars")
                .data(data)
                .enter()
                .append("rect")
                .on("mouseover", function(d) {
                    activeDistrict = d.Country,
                    //console.log(activeDistrict),
                    map.selectAll("path")
                    .data(b.features)
                    .each(function(d) {
                        if (d){
                            //console.log("activeDistrict = " + activeDistrict)
                            if (d.properties.ADMIN == activeDistrict){
                                d3.select(this).style("stroke", "blue").style("stroke-width", "3");
                                console.log(d.properties.ADMIN + "=" + activeDistrict)

                            }
                        }
                    });

And here is my entire JS:

<script>
window.onload = setMap();
function setMap(){  



d3.csv("/data/blah.csv").then(function(data) {
        //console.log(data);
d3.json("/data/blah.topojson").then(function(data2) {
        //console.log(data2);
//Code with data here
    var width = window.innerWidth * 0.5, // 960
        height = 460;
    var activeDistrict;
    //chart vars
    var chartWidth = window.innerWidth * 0.425,
            chartHeight = 473,
            leftPadding = 25,
            rightPadding = 2,
            topBottomPadding = 5,
            chartInnerWidth = chartWidth - leftPadding - rightPadding,
            chartInnerHeight = chartHeight - topBottomPadding * 2,
            translate = "translate(" + leftPadding + "," + topBottomPadding + ")";
     var yScale = d3.scaleLinear()
            .range([0, chartHeight])
            .domain([0, 2000]); 

    //create new svg container for the map
    var map = d3.select("body")
        .append("svg")
        .attr("class", "map")
        .attr("width", width)
        .attr("height", height);

    //create new svg container for the chart
    var chart = d3.select("body")
        .append("svg")
        .attr("width", chartWidth)
        .attr("height", chartHeight)
        .attr("class", "chart");

    //create Albers equal area conic projection centered on France
    var projection = d3.geoNaturalEarth1()
        .center([0, 0])
        .rotate([-2, 0, 0])
        //.parallels([43, 62])
        .scale(175)
        .translate([width / 2, height / 2]);
    var path = d3.geoPath()
        .projection(projection);
       //translate TopoJSON
    d3.selectAll(".boundary")
    .style("stroke-width", 1 / 1);

    var b = topojson.feature(data2, data2.objects.ne_10m_admin_0_countries);
    //console.log(b)
    //console.log(b.features[1].properties.ADMIN) //country name
    var graticule = d3.geoGraticule();

    var attrArray = ["blah blah blah"];

    function joinData(b, data){
    //loop through csv to assign each set of csv attribute values to geojson region
        for (var i=0; i<data.length; i++){
            var csvRegion = data[i]; //the current region
            var csvKey = data[i].Country; //the CSV primary key
              //console.log(data[i].Country)
        //loop through geojson regions to find correct region
            for (var a=0; a<b.features.length; a++){     
                var geojsonProps = b.features[a].properties; //gj props
                var geojsonKey = geojsonProps.ADMIN; //the geojson primary key
                //where primary keys match, transfer csv data to geojson properties object
                if (geojsonKey == csvKey){
                    //assign all attributes and values
                    attrArray.forEach(function(attr){
                        var val = parseFloat(csvRegion[attr]); //get csv attribute value
                        geojsonProps[attr] = val; //assign attribute and value to geojson properties
                    });
                };

            };
        };
        return b;
  };
    joinData(b,data);


    var tooltip = d3.select("body").append("div") 
        .attr("class", "tooltip")       
        .style("opacity", 0);

    //Dynamically Call the current food variable to change the map
    var currentFood = "Beef2";


    var valArray = [];
    data.forEach(function(element) {
        valArray.push(parseInt(element[currentFood]));
    });

    var currentMax = Math.max.apply(null, valArray.filter(function(n) { return !isNaN(n); }));
    console.log("Current Max Value is " + currentMax + " for " + currentFood)



    var color = d3.scaleQuantile()
        .domain(d3.range(0, (currentMax + 10)))
        .range(d3.schemeReds[7]); 




    function drawMap(currentMax){

        d3.selectAll("path").remove();
        // Going to need to do this dynamically
        // Set to ckmeans
        var color = d3.scaleQuantile()
            .domain(d3.range(0, currentMax))
            .range(d3.schemeReds[7]);  

        //console.log(b[1].Beef1)


        map.append("path")
            .datum(graticule)
            .attr("class", "graticule")
            .attr("d", path);

        map.append("path")
            .datum(graticule.outline)
            .attr("class", "graticule outline")
            .attr("d", path);


    console.log(map.selectAll("path").size())

       map.selectAll("countries")
            .data(b.features)
            .enter()
            .append("path")
            .attr("d", path)
            //.style("stroke", "black")
            .on("mouseover", function(d) { 
                activeDistrict = d.properties.ADMIN,
                chart.selectAll("rect")
                .each(function(d) {
                    if(d){
                        //console.log("activeDistrict = " + activeDistrict)
                        if (d.Country == activeDistrict){
                            console.log("confirmed" + d.Country)
                            d3.select(this).style("stroke", "blue").style("stroke-width", "3");

                        }
                    }
                })
                tooltip.transition()    //(this.parentNode.appendChild(this))
                .duration(200)    
                .style("opacity", .9)
                .style("stroke-opacity", 1.0);    
                tooltip.html(d.properties.ADMIN + "<br/>"  + d.properties[currentFood] + "(kg/CO2/Person/Year)")  
                .style("left", (d3.event.pageX) + "px")   
                .style("top", (d3.event.pageY - 28) + "px");
              })          
              .on("mouseout", function(d) {
                activeDistrict = d.properties.ADMIN,
                chart.selectAll("rect")
                .each(function(d) {
                    if (d){
                        //console.log("activeDistrict = " + activeDistrict)
                        if (d.Country == activeDistrict){
                            d3.select(this).style("stroke", "none").style("stroke-width", "0");

                        }
                    }
                })
                tooltip.transition()    
                .duration(500)    
                .style("opacity", 0)
                .style("stroke-opacity", 0); 
              })
            .style("fill", function(d) { return color(d.properties[currentFood]) });
    };

    drawMap(currentMax);
    console.log("sum", d3.sum(valArray))
    //console.log(map.selectAll("path")._groups[0][200].__data__.properties.ADMIN)

    function setChart(data, data2, currentMax, valArray){

        d3.selectAll("rect").remove();
        d3.selectAll("text").remove();

        var color = d3.scaleQuantile()
            .domain(d3.range(0, (currentMax + 10)))
            .range(d3.schemeReds[7]); 
        var chartBackground = chart.append("rect2")
            .attr("class", "chartBackground")
            .attr("width", chartInnerWidth)
            .attr("height", chartInnerHeight)
            .attr("transform", translate);

        var yScale = d3.scaleLinear()
            .range([0, chartHeight])
            .domain([0, (currentMax+10)]);

        var chartTitle = chart.append("text")
            .attr("x", 20)
            .attr("y", 40)
            .attr("class", "chartTitle")
            .text(currentFood.slice(0, -1));
        var chartSub = chart.append("text")
            .attr("x", 20)
            .attr("y", 90)
            .attr("class", "chartSub")
            .text((d3.sum(valArray)*76) + " Billion  World Total");

            // Place Axis at some point
        var bars = chart.selectAll(".bars")
            .data(data)
            .enter()
            .append("rect")
            .on("mouseover", function(d) {
                activeDistrict = d.Country,
                //console.log(activeDistrict),
                map.selectAll("path")
                .data(b.features)
                .each(function(d) {
                    if (d){
                        //console.log("activeDistrict = " + activeDistrict)
                        if (d.properties.ADMIN == activeDistrict){
                            d3.select(this).style("stroke", "blue").style("stroke-width", "3");
                            console.log(d.properties.ADMIN + "=" + activeDistrict)

                        }
                    }
                });
                tooltip.transition()    //(this.parentNode.appendChild(this))
                .duration(200)    
                .style("opacity", .9)
                .style("stroke-opacity", 1.0);    
                tooltip.html(d.Country + "<br/>"  + d[currentFood] + "(kg/CO2/Person/Year)")  
                .style("left", (d3.event.pageX) + "px")   
                .style("top", (d3.event.pageY - 28) + "px");  
              })          
              .on("mouseout", function(d) {  
                map.selectAll("path")
                .data(b.features)
                .each(function(d) {
                    if (d){
                        //console.log("activeDistrict = " + activeDistrict)
                        if (d.properties.ADMIN == activeDistrict){
                            d3.select(this).style("stroke", "none").style("stroke-width", "0");
                            console.log(d.properties.ADMIN + "=" + activeDistrict)

                        }
                    }
                });
                tooltip.transition()    
                .duration(500)    
                .style("opacity", 0)
                .style("stroke-opacity", 0); 
              })
            .sort(function(a, b){
            return a[currentFood]-b[currentFood]
            })
            .transition() //add animation
                .delay(function(d, i){
                    return i * 5
                })
                .duration(1)
            .attr("class", function(d){
                return "bars" + d.Country;
            })
            .attr("width", chartWidth / data.length - 1)
            .attr("x", function(d, i){
                return i * (chartWidth / data.length);
            })
            .attr("height", function(d){
                return yScale(parseFloat(d[currentFood]));
            })
            .attr("y", function(d){
                return chartHeight - yScale(parseFloat(d[currentFood]));
            })
            .style("fill", function(d){ return color(d[currentFood]); }); 

    };

    setChart(data, data2, currentMax, valArray);  

    function createDropdown(data){
        //add select element
        var dropdown = d3.select("body")
            .append("select")
            .attr("class", "dropdown")
            .on("change", function(){
            changeAttribute(this.value, data)
            });

        //add initial option
        var titleOption = dropdown.append("option")
            .attr("class", "titleOption")
            .attr("disabled", "true")
            .text("Select Attribute");

        //add attribute name options
        var attrOptions = dropdown.selectAll("attrOptions")
            .data(attrArray)
            .enter()
            .append("option")
            .attr("value", function(d){ return d })
            .text(function(d){ return d });
    };
    createDropdown(data);

    function changeAttribute(attribute, data){
        //change the expressed attribute
        currentFood = attribute;
        var valArray = [];
        data.forEach(function(element) {
            valArray.push(parseInt(element[currentFood]));
        });

        var currentMax = Math.max.apply(null, valArray.filter(function(n) { return !isNaN(n); }));
        console.log("Current Max Value is " + currentMax + " for " + currentFood)

        // Set a dynamic color range

        var color = d3.scaleQuantile()
            .domain(d3.range(0, currentMax))
            .range(d3.schemeReds[7]); 

        //recolor enumeration units
        drawMap(currentMax);
        //reset chart bars
        setChart(data, data2, currentMax, valArray);

    };

}); //csv
}); //json



}; // end of setmap 

Solution

  • When drawing countries initially you use:

    map.selectAll("countries")
      .data(b.features)
      .enter()
      .append("path")
    

    As there are no elements with the tag countries on your page, the initial selection is empty, and .enter().append("path") creates a path for each item in your data array.

    But when you do a mouseover on the bars you re-assign the data with a selectAll().data() sequence, but you do it a bit differently:

    map.selectAll("path")
      .data(b.features)
      ...
    

    There are paths in your map that aren't countries: the graticule and the outline. Now we've selected all the paths and assigned new data to them. Since the first two items in the selection are the graticule and the outline they now have the data of the first two items in the data array. All the countries will have bound data of a country that is two away from them in the data array. This is why the wrong data will be highlighted when mouseovering the bars and afterwards why the country tooltips are wrong.

    It is not clear why you update the data (I don't see it changing), you could append the countries as so:

    var countries = map.selectAll("countries")
      .data(b.features)
      .enter()
      .append("path")
      ... continue as before
    

    or

    map.selectAll("countries")
      .data(b.features)
      .enter()
      .append("path")
      .attr("class","country")
      ... continue as before
    

    And then in the mouseover function of the bars use:

    countries.each(....
    

    or

    map.selectAll(".country").each(...
    

    Either way still lets you update the data with .data() if needed.


    I'll note that the each method isn't necessary, but may be preferable in some situations, by the looks of it you could use:

    var bars = chart.selectAll(".bars")
      .data(data)
      .enter()
      .append("rect")
      .on("mouseover", function(d) {
         activeDistrict = d.Country,       
         map.selectAll(".country")
           .data(b.features)
           .style("stroke", function(d) { 
             if (d.properties.ADMIN == activeDistrict) return "blue"; else return color(d.properties[currentFood]) 
           })
           .style("stroke-width", function(d) { 
             if (d.properties.ADMIN == activeDistrict) return "3" else return 0; 
           });
      })                          
      ...