javascript - 更新使用双重嵌套数据绑定(bind)的 d3.js (v5) 选择

标签 javascript d3.js svg

我在 d3 中有一个双重嵌套数据集。我想创建一个散点图,以针对第一个键(“时间”变量)的每个值进行更新,但每个点的数据都绑定(bind)到第二个键(“空间”变量)的值。因此,需要明确的是,每个点都应该转换为新的坐标,并且其半径也必须更新。

这是一个数据样本(在文件“prosperLoanData.csv”中)

BorrowerState,LoanOriginationDate,LoanOriginalAmount,LenderYield
AK,2007-01-01 00:00:01,1000,0.1
AK,2007-01-01 00:00:01,2000,0.11
AK,2007-01-01 00:00:01,1500,0.09
AK,2007-01-01 00:00:01,500,0.1
AK,2008-01-01 00:00:01,2500,0.07
AK,2008-01-01 00:00:01,3000,0.06
AK,2008-01-01 00:00:01,3500,0.0652
OK,2007-01-01 00:00:01,4000,0.08
OK,2007-01-01 00:00:01,4100,0.081
OK,2007-01-01 00:00:01,4500,0.0812
OK,2007-01-01 00:00:01,4600,0.0799
OK,2007-01-01 00:00:01,3900,0.08
OK,2008-01-01 00:00:01,5000,0.05

这是我的代码。我虽然删除 YearGroups1.exit().remove() 就足够了,但事实并非如此。我无法正确选择圆圈并将它们重新绑定(bind)到新数据集(在第二个嵌套级别)。你能帮我吗?

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v5.min.js"></script>
    <style>

      div.tooltip { 
        position: absolute;     
        text-align: center;     
        width: 180px;          
        height: 45px;         
        padding: 2px;       
        font: 12px sans-serif;    
        background: lightsteelblue; 
        border: 0px;    
        border-radius: 8px;     
        pointer-events: none;     
    }

    </style>
    <script type="text/javascript">  
      </script>
  </head>
<body>
  <script type="text/javascript">
  /*
    Use D3 to load the loan data
    */

var parseTime = d3.timeParse("%Y-%m-%d %H:%M:%S")

// load data
d3.csv("prosperLoanData.csv").then(function(data) {

  // Setting global parameters

  "use strict";

  //    ----> Margin & size
  var margin = 75,
    width = 1400 - margin,
    height = 600 - margin,
    op = 0.5,
    years = [2007, 2008];

  //    ----> Format for strings
  var formatDec = d3.format(".0f"),
    formatper = d3.format(".2%"),
    formatM = d3.format("\$.2s");


  // change string (from CSV) into number format
  data.forEach( function(d) {
    d["Year"] = parseTime(d.LoanOriginationDate).getYear() + 1900;
    d["LoanOriginalAmount"] = +d["LoanOriginalAmount"];
    d["LenderYield"] = +d["LenderYield"];
    // debugger
    return d; });

  // Function definition

  // function update() {};
  function key_func(d) {
                return d['key'];
            }

  // FIXED PART

  // Define the svg element
  var svg = d3.select("body")
      .append("svg")
      .attr("width", width + margin)
      .attr("height", height + margin);

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

  // Data preprocessing - grouping

  var nested_year = d3.nest()
    .key( function(d) { return d.Year } )
    .key( function(d) { return d.BorrowerState })
    .rollup( function(points) {

      // debugger
          var mean_loan = d3.mean(points, function(d) { return d.LoanOriginalAmount; } );
          var mean_lenderyield = d3.mean(points, function(d) { return d.LenderYield; } );
          // var std_loan = d3.deviation(points, function(d) { return d.LoanOriginalAmount; } );
          var sum_loan = d3.sum(points, function(d) { return d.LoanOriginalAmount; } );

          var max_loan = d3.max(points, function(d) { return d.LoanOriginalAmount; } );
          var min_loan = d3.min(points, function(d) { return d.LoanOriginalAmount; } );

          var max_ly = d3.max(points, function(d) { return d.LenderYield; } );
          var min_ly = d3.min(points, function(d) { return d.LenderYield; } );

          // debugger

          return {
            "meanLoan" : mean_loan,
            "meanLenderyield" : mean_lenderyield,
            // "stdLoan" : std_loan,
            "sumLoan" : sum_loan,
            };

           }

         )
    .entries(data);

  // Determining X/Y Max & Min

  var LOA_E1 = d3.min(nested_year, function(d) {return d3.min(d.values, function(da) { return da.value.meanLoan; });})
  var LOA_E2 = d3.max(nested_year, function(d) {return d3.max(d.values, function(da) { return da.value.meanLoan; });})

  var LY_E1 = d3.min(nested_year, function(d) {return d3.min(d.values, function(da) { return da.value.meanLenderyield; });})
  var LY_E2 = d3.max(nested_year, function(d) {return d3.max(d.values, function(da) { return da.value.meanLenderyield; });})

  var LenderYield_Extent        = [LY_E1  , LY_E2 ];
  var LoanOriginalAmount_Extent = [LOA_E1 , LOA_E2];

  // Creating a scale
  var XScale = d3.scaleLinear()
    .range([margin , width])
    .domain([ 1.05 * LenderYield_Extent[0] - 0.05 * LenderYield_Extent[1] , 
              1.05 * LenderYield_Extent[1] - 0.05 * LenderYield_Extent[0] ] );

  // debugger

  var YScale = d3.scaleLinear()
    .range([height , margin])
    .domain([ 0, 1.025 * LoanOriginalAmount_Extent[1]]);

  var SUM_LOAN_Extent = [70E3 , 1.2E7]; // d3.extent(red_data.value, d => d.value.sumLoan);

  var radius = d3.scaleSqrt()
    .domain(SUM_LOAN_Extent)
    .range([3,0.375 * margin/2]);

  // Creating axis

  var x_axis = d3.axisBottom()
    .scale(XScale)
    .tickFormat(d3.format(".2%"));

  var y_axis = d3.axisLeft()
    .scale(YScale)
    .tickFormat(d3.format(".0f"));

  svg.append('g')
    .attr('transform', "translate(0," + height + ")")
    .call(x_axis);

  svg.append('g')
    .attr('transform', "translate(" + margin + ",0)")
    .call(y_axis);

  // Text label for the x axis
  svg.append("text")             
    .attr("transform",
          "translate(" + (width/2 + margin) + " ," + 
                         (height + margin/2) + ")")
    .style("text-anchor", "middle")
    .style("font-size", 20)
    .text("Mean lender yield (percentage)");

  // Text label for the y axis
  svg.append("text")
    .attr("transform", "rotate(-90)")
    .attr("y", 0)
    .attr("x",0 - (height / 2))
    .attr("dy", "1em")
    .style("text-anchor", "middle")
    .style("font-size", 20)
    .text("Mean loan original amount [US dollars]"); 

  // Creating gridlines

function make_x_gridlines() {   
    return d3.axisBottom(XScale)
        .ticks(5);
};

function make_y_gridlines() {   
    return d3.axisLeft(YScale)
        .ticks(5);
};

svg.append("g")     
    .attr("class", "grid")
    .attr("transform", "translate(0," + height + ")")
    .call(make_x_gridlines()
      .tickSize(-(height - 1 * margin ))
      .tickFormat(""));

svg.append("g")     
    .attr("class", "grid")
    .attr('transform', "translate(" + margin + ",0)")
    .call(make_y_gridlines()
      .tickSize(-(width -  1 * margin ))
      .tickFormat(""));

// Add legend

var DELTA = SUM_LOAN_Extent[1] - SUM_LOAN_Extent[0];

var valuesToShow = [SUM_LOAN_Extent[0] + 0.00 * DELTA, SUM_LOAN_Extent[0] + 0.25 * DELTA ,
                    SUM_LOAN_Extent[0] + 0.50 * DELTA, SUM_LOAN_Extent[0] + 0.75 * DELTA ,
                    SUM_LOAN_Extent[0] + 1.00 * DELTA ];

var xCircle = width + 0.35 * margin;
var xLabel = 200;
var yCircle = 150;

var legend = svg.append("g")
  .attr("class","legend")
  .attr("trasform","translate(" + (width - 100) + "," + 20 + ")" )
  .selectAll("g")
  .data(valuesToShow)
  .enter()
  .append("g");

legend.append("circle")
  .attr("cy", function(d,i) { 
     return (i+1)* 0.4 * yCircle + radius(d)/2; })
  .attr("cx", xCircle)
  .attr("r", function(d) {
     return radius(d); })
  .attr("stroke","black")
  .attr("stroke-width",1.)
  .attr("opacity", op);

// Add legend: labels
svg.selectAll("legend")
  .data(valuesToShow)
  .enter()
  .append("text")
    .attr('x', function(d,i){ return width + 0.55 * margin; } )
    .attr('y', function(d,i){ return (i+1)* 0.4 * yCircle + radius(d)/2 } )
    .text( function(d){ return formatM(d) } )
    .style("font-size", 10)
    .attr('alignment-baseline', 'middle');

svg.append("text")
    .attr('x', 0.98 * xCircle )
    .attr('y', 0.15 * yCircle )
    .text( "Size = " )
    .style("font-size", 12.5)
    .attr('alignment-baseline', 'middle');

svg.append("text")
    .attr('x', 0.98 * xCircle )
    .attr('y', 0.15 * yCircle + 10 )
    .text( "Total loan" )
    .style("font-size", 12.5)
    .attr('alignment-baseline', 'middle');

svg.append("text")
    .attr('x', 0.98 * xCircle )
    .attr('y', 0.15 * yCircle + 20)
    .text( "amount (US \$)" )
    .style("font-size", 12.5)
    .attr('alignment-baseline', 'middle');

// Add color legend

var colors = d3.scaleOrdinal()
  .domain([0, 1, 2])
  .range(["blue", "green" , "black"]);

var stat = d3.scaleOrdinal()
  .domain([0, 1, 2])
  .range(["CA", "TX" , "Others"]);

var aaa = [ SUM_LOAN_Extent[0] + 1.00 * DELTA , SUM_LOAN_Extent[0] + 1.00 * DELTA, SUM_LOAN_Extent[0] + 1.00 * DELTA];

var legend1 = svg.append("g")
  .attr("class","legend")
  .attr("trasform","translate(" + (width - 100) + "," + 20 + ")" )
  .selectAll("g")
  .data(aaa)
  .enter()
  .append("g");

legend1.append("circle")
  .attr("cy", function(d,i) { //debugger
     return (6+i)* 0.4 * yCircle + radius(d)/2; })
  .attr("cx", xCircle)
  .attr("r", function(d) {
     return radius(d); })
  .attr("fill", function(d,i){ return colors(i)})
  .attr("opacity", op);

// Add legend: labels
svg.selectAll(".legend1")
  .data(aaa)
  .enter()
  .append("text")
    .attr('x', function(d,i){ return width + 0.55 * margin; } )
    .attr('y', function(d,i){ return (6+i)* 0.4 * yCircle + radius(d)/2 } )
    .text( function(d,i){ return stat(i) } )
    .style("font-size", 10)
    .attr('alignment-baseline', 'middle');

// Appending first circles

// Accessing 1st group

var YearGroups = svg.selectAll(".YearGroups")
    .data(nested_year, key_func)
    .enter()
    .append("g")
    .attr("class", "YearGroups");

// Accessing 2nd group

var circles = YearGroups.selectAll("circle")
    .data(function(d){ 
      return d.values 
    })
    .enter()
    .append("circle")
    .transition()
    .duration(500)
    .attr("cx", function(d) { return XScale(d.value.meanLenderyield) } )
    .attr("cy", function(d) { return YScale(d.value.meanLoan       ) } )
    .attr("r" , function(d) { return radius(d.value.sumLoan        ) } )
    .attr("fill" , function(d) { if 
                                                    (d.key == "CA") {return "blue"}
                                                  if (d.key == "TX") {return "green"}
                                                  else
                                                    { return "black" }})
    .attr("opacity" , op);

// debugger

// VARIABLE PART

function update(year) {

              var filt = nested_year.filter(function(d) {return d.key == year;} );

              var YearGroups1 = svg.selectAll(".YearGroups")
                .data(filt, key_func);

              YearGroups1.exit().remove();

              var circles = YearGroups1.enter().append('g')
                               .attr("class", "YearGroups").selectAll('circle')
                               .data(function(d){ return d.values });

              var CircPl = circles.enter()
                     .append("circle")
                     .transition()
                     .duration(500)
                     .attr("cx", function(d) { // debugger
                                               return XScale(d.value.meanLenderyield); })
                     .attr("cy", function(d) { return YScale(d.value.meanLoan       ); })
                     .attr("r" , function(d) { return radius(d.value.sumLoan        ); })
                     .attr("fill" , function(d) { if 
                                                    (d.key == "DC") {return "blue"}
                                                  if (d.key == "AR") {return "green"}
                                                  else
                                                    { return "black" }})
                     .attr("opacity" , op)
                     .on("mouseover", function(d) {
                       div.transition()
                          .duration(200)
                          .style("opacity", .9);    
                       div.html("Lender yield : " + formatper(d.value.meanLenderyield) + "<br/>"  + 
                                "Loan original amount : " + formatDec(d.value.meanLoan) + " $ <br/>"  + 
                                "State : " + d.key)
                          .style("left", (d3.event.pageX) + "px")   
                          .style("top", (d3.event.pageY - 28) + "px");  
                     })          
                     .on("mouseout", function(d) {   
                       div.transition()    
                         .duration(500)    
                         .style("opacity", 0);
                     });

              CircPl


              // debugger

              // Chart Title
              svg.selectAll(".Title").remove()

              svg
                .append("text")
                .attr("class","Title")
                .attr("x", (margin + width) / 2)
                .attr("y", margin / 2)
                .attr("font-weight", "bold")
                .attr("text-anchor", "middle")
                .style("font-size", "32px")
                .text("US Loans per State in " + year); // Title updated
  }

var year_idx = 0;

var year_interval = setInterval(function() {
  update(years[year_idx]);

  year_idx++;

  if(year_idx >= years.length) {
      clearInterval(year_interval);
  }
}, 1000);


});

  </script>
</body>
</html>

最佳答案

我找到了该问题的解决方案(感谢 Udacity Mentor 团队的 Myles C.)。

事实上,要访问的数据很容易存储在虚拟变量中,该变量随后用于数据绑定(bind),并且 .merge() 语句用于将两者更新到所有圆。

function update_video(year) {

    var filt = nested_year.filter(function(d) {
      return d.key == year;
    });

    var data = filt[0].values

    // Selection and Bind Data
    const update = YearGroups.selectAll("circle")
      .data(data);

    var upd = update.enter()
      .select("circle")
      .merge(update)
      .transition()
      .duration(1500)
      .attr("class", "node_circle")
      .attr("cx", function(d) {
        return XScale(d.value.meanLenderyield);
      })
      .attr("cy", function(d) {
        return YScale(d.value.meanLoan);
      })
      .attr("r", function(d) {
        return radius(d.value.sumLoan);
      })
      .attr("fill", function(d) {
        if (d.key == "CA") { return "blue";}
        if (d.key == "TX") { return "green";}
        else { return "black"}
      });

    // Command necessary to remove States that are not available in the
    // database for that year
    update.exit().remove();


    // Chart Title
    svg.selectAll(".Title").remove();

    svg.append("text")
      .attr("class", "Title")
      .attr("x", (margin + width) / 2)
      .attr("y", margin / 2)
      .attr("font-weight", "bold")
      .attr("text-anchor", "middle")
      .style("font-size", "32px")
      .text("US Loans per State in " + year); // Title update
  };

使用此函数,无论状态如何(嵌套数据的第二个键),每个圆都会更新; .exit().remove() 命令已用于删除所有未绑定(bind)到数据的点。

然后,使用第二个函数让用户选择年份并更新圆圈而无需过渡,但添加工具提示以获取用鼠标悬停在点上的数据信息。

function update_with_tooltip(year) {

    var filt = nested_year.filter(function(d) {
      return d.key == year;
    });

    var data = filt[0].values;

    // Selection and Bind Data
    const update = YearGroups.selectAll("circle")
      .data(data);

    // Update and merge
    var upd = update.enter()
      .select("circle")
      .merge(update)
      .attr("class", "node_circle")
      .attr("cx", function(d) {
        return XScale(d.value.meanLenderyield);
      })
      .attr("cy", function(d) {
        return YScale(d.value.meanLoan);
      })
      .attr("r", function(d) {
        return radius(d.value.sumLoan);
      })
      .attr("fill", function(d) {
        if (d.key == "CA") { return "blue";}
        if (d.key == "TX") { return "green";}
        else { return "black"}
      })
      .on("mouseover", function(d) {
        div.transition()
          .duration(200)
          .style("opacity", .9);    

        div.html("Lender yield : " + formatper(d.value.meanLenderyield) + 
          "<br/>"  + "Loan original amount : " + formatDec(d.value.meanLoan)
          + " $ <br/>"  + "State : " + d.key)
          .style("left", (d3.event.pageX) + "px")
          .style("top", (d3.event.pageY - 28) + "px");
      })
      .on("mouseout", function(d) {
        div.transition()    
          .duration(500)    
          .style("opacity", 0);
      });

    // Command necessary to remove States that are not available in the
    // database for that year
    update.exit().remove();

    // Chart Title
    svg.selectAll(".Title").remove();

    svg.append("text")
      .attr("class", "Title")
      .attr("x", (margin + width) / 2)
      .attr("y", margin / 2)
      .attr("font-weight", "bold")
      .attr("text-anchor", "middle")
      .style("font-size", "32px")
      .text("US Loans per State in " + year); // Title update
  }

关于javascript - 更新使用双重嵌套数据绑定(bind)的 d3.js (v5) 选择,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60442199/

相关文章:

javascript - javascript 中的日期格式格式错误

javascript - 如何遍历树并过滤javascript中的匹配节点?

javascript - D3.js/Dc.js 折线图中单线的不同颜色线段

javascript - d3 节点内的 Angular 指令

javascript - 对未知嵌套对象进行类型保护

javascript - 排序后如何显示父级的新顺序?

json - 使用新数据启动 D3.js 数据映射

android - 我如何给这个 PictureDrawable 着色?

javascript - 放大 viewbox svg - Mapael

javascript - D3 数据映射 : OnClick Events on Bubbles