D3 карта + скоординированная визуализация графика: выбор неправильных стран / изменение карты

Вот моя проблема: у меня есть карта SVG и гистограмма SVG. Я хочу создать скоординированный выбор. Например, пользователь выделяет столбец на графике, а страна на карте, соответствующая этим данным, также выделяется и наоборот.

Прямо сейчас я могу выделить любую страну на карте, и соответствующая соответствующая полоса также будет выделена. Однако то же самое не работает для баров. Когда я выделяю полосу, подсвечивается случайная страна, и после этого названия стран, отображаемые во всплывающей подсказке, смешиваются и вводятся неверно.

Вот карта -> основной момент гистограммы:

...
           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");

                            }
                        }
                    })

...

Вот гистограмма -> подсветка карты. Это функция, которую я не могу вести себя правильно.

            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)

                            }
                        }
                    });

А вот и весь мой 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 

2 ответа

Решение

При рисовании стран изначально вы используете:

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

Так как нет элементов с тегом countries на вашей странице, первоначальный выбор пуст, и .enter().append("path") создает путь для каждого элемента в вашем массиве данных.

Но когда вы наводите курсор мыши на панели, вы переназначаете данные с помощью selectAll().data() последовательность, но вы делаете это немного по-другому:

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

На вашей карте есть пути, которые не являются странами: сетка и план. Теперь мы выбрали все пути и присвоили им новые данные. Поскольку первые два элемента в выделении - это сетка и контур, теперь у них есть данные первых двух элементов в массиве данных. Все страны будут иметь привязанные данные о стране, которая находится на расстоянии двух от них в массиве данных. Вот почему неправильные данные будут выделены при наведении курсора на указатель мыши, а затем, почему всплывающие подсказки по стране неверны.

Непонятно, почему вы обновляете данные (я не вижу их изменения), вы можете добавить страны следующим образом:

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

или же

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

А затем в функции наведения мыши из панелей используйте:

countries.each(....

или же

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

В любом случае вы можете обновить данные с помощью .data() если нужно.


Отмечу, что each Метод не обязателен, но может быть предпочтительным в некоторых ситуациях, по внешнему виду вы можете использовать:

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; 
       });
  })                          
  ...

Вы можете попробовать следующее, чтобы быть более последовательным в том, что вы создаете и что вы выбираете.

Карта:

  • выберите по классу, потому что у вас есть больше path в SVG
  • добавить класс в строку, чтобы выделить вместо установки стиля

       map.selectAll(".countries")
            .data(b.features)
            .enter()
            .append("path")
            .attr("class", "countries")
            .attr("d", path)
            //.style("stroke", "black")
            .on("mouseover", function(d) { 
                activeDistrict = d.properties.ADMIN,
                chart.selectAll(".bars")
                     .classed("highlight", function(d) {
                         return d && d.Country === activeDistrict;
                      });
                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(".bars")
                     .classed("highlight", false);
                tooltip.transition()    
                .duration(500)    
                .style("opacity", 0)
                .style("stroke-opacity", 0); 
              })
            .style("fill", function(d) { return color(d.properties[currentFood]) });
    };
    

Рисование баров

  • исправить класс, добавить пробел между bars а также country
  • не привязывать новые данные к путям карты при наведении мыши и
  • выбрать пути к карте по классу, а не по типу
  • добавить класс к пути карты, чтобы выделить вместо установки стиля

        var bars = chart.selectAll(".bars")
            .data(data)
            .enter()
            .append("rect")
            .on("mouseover", function(d) {
                activeDistrict = d.Country;
                //console.log(activeDistrict),
                map.selectAll(".countries")
                    .classed("highlight", function(d) {
                        return d && 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(".countries")
                   .classed("highlight", false);
                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]); }); 
    
Другие вопросы по тегам