How does D3 change detection work?

后端 未结 1 514
春和景丽
春和景丽 2020-12-21 22:27

If I have an array of JSON objects. How does D3 determine which ones go in the enter() set?

If I have an array of objects, like so:

var          


        
相关标签:
1条回答
  • 2020-12-21 23:14

    If I understand your question correctly, you want to know how D3 associates a given object to a given DOM element.

    By default, if you don't set a key function (more on that below), the objects are associated by their order. According to the API:

    ...the first datum in data is assigned to the first selected element, the second datum to the second selected element, and so on.

    Let's see this in the following example. The first array is this:

    var data = [{
        label: 'a',
        value: 1
    }, {
        label: 'b',
        value: 3
    }, {
        label: 'c',
        value: 2
    }];
    

    The second array has, also, 3 objects. But pay attention to the change: the first one is the label c, and the third one is the label a. They are swapped:

    var data2 = [{
        label: 'c',
        value: 99
    }, {
        label: 'b',
        value: 2
    }, {
        label: 'a',
        value: 3
    }];
    

    Because of that, the bar relative to a in the first data will get the value of c in the second one. Check the demo, clicking the button:

    var svg = d3.select("svg");
    
    var data = [{
      label: 'a',
      value: 1
    }, {
      label: 'b',
      value: 3
    }, {
      label: 'c',
      value: 2
    }];
    
    var data2 = [{
      label: 'c',
      value: 99
    }, {
      label: 'b',
      value: 2
    }, {
      label: 'a',
      value: 3
    }];
    
    var xScale = d3.scaleLinear()
      .domain([0, d3.max(data, d => d.value)])
      .range([0, 260]);
    
    var yScale = d3.scaleBand()
      .domain(data.map(d => d.label))
      .range([10, 140])
      .padding(0.3);
    
    var rects = svg.selectAll("foo")
      .data(data)
      .enter()
      .append("rect");
    
    rects.attr("x", 40)
      .attr("y", d => yScale(d.label))
      .attr("height", yScale.bandwidth)
      .attr("width", d => xScale(d.value))
      .attr("fill", "teal");
      
    d3.axisLeft(yScale)(svg.append("g").attr("transform", "translate(40,0)"))
    
    d3.select("button").on("click", function() {
    
      xScale.domain([0, d3.max(data2, d => d.value)]);
    
      rects.data(data2);
    
      rects.transition()
        .duration(1000)
        .attr("width", d => xScale(d.value))
    
    })
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <button>Click</button>
    <br>
    <svg></svg>

    As you can see, the chart after clicking the button is clearly incorrect.

    To avoid that, making sure that the DOM element previously bound to the label a will still be bound to that label a when the data array changes, we have to use a key function:

    A key function may be specified to control which datum is assigned to which element, replacing the default join-by-index. This key function is evaluated for each selected element, in order, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i]). The key function is then also evaluated for each new datum in data, being passed the current datum (d), the current index (i), and the group’s new data, with this as the group’s parent DOM element. The datum for a given key is assigned to the element with the matching key. If multiple elements have the same key, the duplicate elements are put into the exit selection; if multiple data have the same key, the duplicate data are put into the enter selection.

    In your case, this would be the key function:

    .data(data, d => d.label)
    //key-------^
    

    Check the exact same code, but with a key function:

    var svg = d3.select("svg");
    
    var data = [{
      label: 'a',
      value: 1
    }, {
      label: 'b',
      value: 3
    }, {
      label: 'c',
      value: 2
    }];
    
    var data2 = [{
      label: 'c',
      value: 99
    }, {
      label: 'b',
      value: 2
    }, {
      label: 'a',
      value: 3
    }];
    
    var xScale = d3.scaleLinear()
      .domain([0, d3.max(data, d => d.value)])
      .range([0, 260]);
    
    var yScale = d3.scaleBand()
      .domain(data.map(d => d.label))
      .range([10, 140])
      .padding(0.3);
    
    var rects = svg.selectAll("foo")
      .data(data, d => d.label)
      .enter()
      .append("rect");
    
    rects.attr("x", 40)
      .attr("y", d => yScale(d.label))
      .attr("height", yScale.bandwidth)
      .attr("width", d => xScale(d.value))
      .attr("fill", "teal");
      
    d3.axisLeft(yScale)(svg.append("g").attr("transform", "translate(40,0)"))
    
    d3.select("button").on("click", function() {
    
      xScale.domain([0, d3.max(data2, d => d.value)]);
    
      rects.data(data2, d => d.label);
    
      rects.transition()
        .duration(1000)
        .attr("width", d => xScale(d.value))
    
    })
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <button>Click</button>
    <br>
    <svg></svg>

    You can see that, despite the order of the objects being different in the new data array (c was the third one, now c is the first one), this doesn't matter, because D3 is binding the data to each DOM element taking into account the label property.

    0 讨论(0)
提交回复
热议问题