d3.js nodes are not exiting properly

2019-06-12 05:41发布

so here goes. I've spent quite some time on this one - and am really tired - so hopefully something silly hasn't eluded me.

I am using a dataset to create a few lines in a chart. After that, using a legend I want to hide lines while also changing the dataset - so changing opacity won't cut it.

I followed the path of adding a key enabled on each object in my dataset and set it to false in order to be able to filter objects to hide. However, It doesn't work as expected since I cannot exit() the lines properly. Removing the lines completely and binding the dataset again gets the job done but messes with the mapping of the line colors to the legend items.

The problem lies somewhere in the redraw() function.

Hopefully someone is able to sort out this nightmare!

https://jsfiddle.net/2en21Lqh/2/

I have created a fiddle

2条回答
We Are One
2楼-- · 2019-06-12 06:04

I understand your feeling... I hope this answer will help you.

You have a few problems in your code, so I fixed the main ones putting comments along the way. Keep in mind there is still a lot that can be improved in my version, up to you to make it better.

I will update the answer with a few comments detailing my thoughts about how to tackle this piece of code and a link to a useful article or answer here on SO.

Meanwhile check this fiddle, and the code below

// .. code above is unchanged 
line = d3.line()
  .x(function(d) {
    return x(d.date);
  }).y(function(d) {
    return y(d.value);
  });

// notice I deleted the part where you created the groups and the path the first time 
// You don't need it anymore

var legendItem = d3.select(".legend")
  .selectAll("li")
  .data(dataGroup)
  .enter()
  .append("li")
  .on('click', function(d) {
    if (d.enabled) {
      d.enabled = false;
    } else {
      d.enabled = true;
    }
    redraw();
  });

legendItem
  .append("span")
  .attr("class","color-square")
  .style("background", function(d, i) {
    // This is basically a hack for this demo 
    // You should find a better way to assign an index to a color
    var idx = +d.key.slice(-1);  
    return colorScale(idx - 1);
  });

legendItem
  .append("span")
  .text(function(d) {
    return (d.values[0].name)
  });

redraw = function() {

  y.domain([
    d3.min(dataGroup, function(c) {
      return d3.min(c.values, function(d) {
        return d.value;
      });
    }),
    // In the demo I don't rescale after the datasets change, so no need to filter
    // by enabled sets. If you want the max to be calculated only for enabled items
    // you will also have to redraw the axis, otherwise you will display incorrect data.
    d3.max(dataGroup, function(c) { 
      return d3.max(c.values, function(d) {
        return d.value;
      });
    })
  ]);

  // Notice that i filter the data passed to the .data() function,
  // not after it.
  lines = svg.selectAll("g.d3-group")
    .data(dataGroup.filter(d => d.enabled))

  // This is the Enter + update selection
  var linesEnter = lines
    .enter()
    .append("g")
    .merge(lines)
    .attr("class", "d3-group")

  // We want the data to go from the <g> to its child, the <path> element
  // To do so, we need to rebind the data.
  var pathJoin = linesEnter
    .selectAll('path')
    .data(d => [d]) // <= this is very important

  // Enter the path and update it
  var pathEnter = pathJoin
    .enter()
    .append("path")
    .attr("class", "d3-line")
    .merge(pathJoin)    
    .attr('d', function(d) {
      return line(d.values);
    })
    // Notice how "i" is not used. If you log it you will see it is always 0
    // as it is always the first child of its parent, the <g class="group">
    .style("stroke", function(d, i) {
      var idx = +d.key.slice(-1);
      return colorScale(idx - 1);
    });


    lines.exit().remove(); 

}

redraw();

If you follow the enter, update, exit pattern you don't need to have two different function to draw and update a chart. The action will be the same, so this means less code duplication. Hence, I removed the first part where you were drawing them at page load

var myline = svg.selectAll(".d3-group")...

An important thing to get right is piping the data from parent (<g class="d3-group">) to child (<path>). This is happening here:

linesEnter
    .selectAll('path')
    .data(d => [d]) // <= here

This way your paths will always be drawn by d3 according to the data that you pass to their respective parents, and will always be in sync.

Also, the reason why the mapping legend/lines gets messed up is because of how you are assigning the colors,i.e. by using indexes.

You will always have 3 labels in the legend, but a variable number of lines in the SVG (1, 2 or 3), so you can't rely on indexes to assign colors. I used a method that is far from perfect but gets the job done. You will want to find a more scalable and reliable one.

Let me know if you need any clarification.

A few links that might help:

查看更多
够拽才男人
3楼-- · 2019-06-12 06:14

Here's your code refactored with the following fixes:

  1. Condensed initial draw and redraw into single function.
  2. Properly handled enter, update, exit pattern in this single function.
  3. Introduced key function to fix data binding (keep it unique to the data).
  4. Use d.key for colors instead of index (to fix the wandering colors).
  5. Fixed some general code quality issues (missing var keywords, filtering on selection instead of data, etc...)

I did not "toggle" the lines using opacity, although that would work as well.

<!DOCTYPE html>
<html>

<head>
  <script data-require="d3@4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
  <script data-require="jquery@3.0.0" data-semver="3.0.0" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.js"></script>
  <style>
    .line-chart {
      width: 800px;
      height: 200px;
    }
    
    .d3-axis {
      font-family: 'Arial', sans-serif;
      font-size: 10px;
    }
    
    .d3-line {
      fill: none;
      stroke-width: 2px;
    }
    
    .d3-axis path {
      fill: none;
      stroke: #e6e6e6;
      shape-rendering: crispEdges;
      opacity: 0; // remove axes
    }
    
    .d3-axis line {
      fill: none;
      stroke: #eee;
      shape-rendering: crispEdges;
    }
    
    ul li {
      display: inline-block;
      margin-left: 10px;
    }
    
    .color-square {
      display: block;
      float: left;
      margin-right: 3px;
      width: 20px;
      height: 20px;
      border: 1px solid #000;
    }
  </style>
</head>

<body>
  <div class="line-chart"></div>
  <ul class="legend"></ul>
  <script>
    var data = [{
      "name": "line1",
      "date": "2016-10-07T23:59:07Z",
      "value": 67
    }, {
      "name": "line1",
      "date": "2016-10-15T11:35:32Z",
      "value": 57
    }, {
      "name": "line1",
      "date": "2017-02-09T07:13:41Z",
      "value": 11
    }, {
      "name": "line1",
      "date": "2016-11-16T21:18:03Z",
      "value": 12
    }, {
      "name": "line1",
      "date": "2016-05-01T03:08:22Z",
      "value": 71
    }, {
      "name": "line1",
      "date": "2016-10-01T08:15:08Z",
      "value": 64
    }, {
      "name": "line1",
      "date": "2016-07-27T09:58:43Z",
      "value": 25
    }, {
      "name": "line1",
      "date": "2016-04-15T12:20:35Z",
      "value": 15
    }, {
      "name": "line1",
      "date": "2016-11-01T11:51:14Z",
      "value": 69
    }, {
      "name": "line1",
      "date": "2016-10-05T23:27:50Z",
      "value": 12
    }, {
      "name": "line1",
      "date": "2016-11-11T21:53:45Z",
      "value": 87
    }, {
      "name": "line1",
      "date": "2017-01-22T17:22:10Z",
      "value": 10
    }, {
      "name": "line1",
      "date": "2016-07-18T23:33:03Z",
      "value": 27
    }, {
      "name": "line1",
      "date": "2017-01-04T14:35:53Z",
      "value": 6
    }, {
      "name": "line1",
      "date": "2016-11-10T07:17:06Z",
      "value": 91
    }, {
      "name": "line1",
      "date": "2016-04-18T00:40:18Z",
      "value": 56
    }, {
      "name": "line1",
      "date": "2016-06-23T11:27:18Z",
      "value": 22
    }, {
      "name": "line1",
      "date": "2016-08-10T12:53:00Z",
      "value": 18
    }, {
      "name": "line1",
      "date": "2016-09-02T17:24:51Z",
      "value": 89
    }, {
      "name": "line1",
      "date": "2016-06-08T03:09:20Z",
      "value": 27
    }, {
      "name": "line1",
      "date": "2016-10-30T17:54:30Z",
      "value": 38
    }, {
      "name": "line1",
      "date": "2017-01-22T01:56:44Z",
      "value": 99
    }, {
      "name": "line1",
      "date": "2016-06-02T19:58:44Z",
      "value": 48
    }, {
      "name": "line1",
      "date": "2016-07-12T01:04:56Z",
      "value": 68
    }, {
      "name": "line1",
      "date": "2016-09-23T07:30:45Z",
      "value": 11
    }, {
      "name": "line1",
      "date": "2016-11-08T05:18:12Z",
      "value": 29
    }, {
      "name": "line1",
      "date": "2017-01-24T03:46:43Z",
      "value": 19
    }, {
      "name": "line2",
      "date": "2016-04-17T06:36:39Z",
      "value": 44
    }, {
      "name": "line2",
      "date": "2016-03-27T17:40:29Z",
      "value": 29
    }, {
      "name": "line2",
      "date": "2016-09-13T02:11:44Z",
      "value": 55
    }, {
      "name": "line2",
      "date": "2016-12-24T10:47:49Z",
      "value": 54
    }, {
      "name": "line2",
      "date": "2016-11-12T21:17:27Z",
      "value": 74
    }, {
      "name": "line2",
      "date": "2016-07-17T10:18:03Z",
      "value": 55
    }, {
      "name": "line2",
      "date": "2016-10-15T10:46:42Z",
      "value": 24
    }, {
      "name": "line2",
      "date": "2016-08-25T12:10:23Z",
      "value": 63
    }, {
      "name": "line2",
      "date": "2017-01-22T18:08:27Z",
      "value": 88
    }, {
      "name": "line2",
      "date": "2016-05-04T09:47:14Z",
      "value": 44
    }, {
      "name": "line2",
      "date": "2016-10-19T18:45:20Z",
      "value": 74
    }, {
      "name": "line2",
      "date": "2017-01-16T19:03:01Z",
      "value": 46
    }, {
      "name": "line2",
      "date": "2017-01-03T18:05:28Z",
      "value": 32
    }, {
      "name": "line2",
      "date": "2016-09-22T14:32:07Z",
      "value": 93
    }, {
      "name": "line2",
      "date": "2016-08-26T16:07:09Z",
      "value": 22
    }, {
      "name": "line2",
      "date": "2016-08-12T07:03:45Z",
      "value": 52
    }, {
      "name": "line2",
      "date": "2016-10-09T11:12:56Z",
      "value": 52
    }, {
      "name": "line2",
      "date": "2016-10-11T00:13:01Z",
      "value": 39
    }, {
      "name": "line2",
      "date": "2016-10-23T16:35:20Z",
      "value": 58
    }, {
      "name": "line2",
      "date": "2016-07-06T05:18:24Z",
      "value": 95
    }, {
      "name": "line2",
      "date": "2017-02-03T08:49:39Z",
      "value": 51
    }, {
      "name": "line2",
      "date": "2016-07-21T00:03:27Z",
      "value": 100
    }, {
      "name": "line2",
      "date": "2016-08-27T07:23:05Z",
      "value": 71
    }, {
      "name": "line3",
      "date": "2016-11-11T21:53:45Z",
      "value": 87
    }, {
      "name": "line3",
      "date": "2017-01-22T17:22:10Z",
      "value": 220
    }, {
      "name": "line3",
      "date": "2016-07-18T23:33:03Z",
      "value": 24
    }, {
      "name": "line3",
      "date": "2017-01-04T14:35:53Z",
      "value": 65
    }, {
      "name": "line3",
      "date": "2016-11-10T07:17:06Z",
      "value": 9
    }, {
      "name": "line3",
      "date": "2016-04-18T00:40:18Z",
      "value": 54
    }, {
      "name": "line3",
      "date": "2016-06-23T11:27:18Z",
      "value": 72
    }, {
      "name": "line3",
      "date": "2016-08-10T12:53:00Z",
      "value": 88
    }, {
      "name": "line3",
      "date": "2016-09-02T17:24:51Z",
      "value": 89
    }, {
      "name": "line3",
      "date": "2016-06-08T03:09:20Z",
      "value": 27
    }, {
      "name": "line3",
      "date": "2016-10-30T17:54:30Z",
      "value": 38
    }, {
      "name": "line3",
      "date": "2017-01-22T01:56:44Z",
      "value": 99
    }, {
      "name": "line3",
      "date": "2016-06-02T19:58:44Z",
      "value": 48
    }, {
      "name": "line3",
      "date": "2016-07-12T01:04:56Z",
      "value": 68
    }, {
      "name": "line3",
      "date": "2016-09-23T07:30:45Z",
      "value": 51
    }, {
      "name": "line3",
      "date": "2016-11-08T05:18:12Z",
      "value": 49
    }, {
      "name": "line3",
      "date": "2017-01-24T03:46:43Z",
      "value": 89
    }, {
      "name": "line3",
      "date": "2016-04-17T06:36:39Z",
      "value": 54
    }, {
      "name": "line3",
      "date": "2016-03-27T17:40:29Z",
      "value": 27
    }, {
      "name": "line3",
      "date": "2016-09-13T02:11:44Z",
      "value": 58
    }, {
      "name": "line3",
      "date": "2016-12-24T10:47:49Z",
      "value": 24
    }, {
      "name": "line3",
      "date": "2016-11-12T21:17:27Z",
      "value": 54
    }, {
      "name": "line3",
      "date": "2016-07-17T10:18:03Z",
      "value": 55
    }, {
      "name": "line3",
      "date": "2016-10-15T10:46:42Z",
      "value": 24
    }, {
      "name": "line3",
      "date": "2016-08-25T12:10:23Z",
      "value": 63
    }]

    margin = {
      top: 20,
      right: 20,
      bottom: 20,
      left: 30
    };

    var containerwidth = $('.line-chart').width(),
        containerheight = $('.line-chart').height();


    var width = containerwidth - margin.left - margin.right,
       height = containerheight - margin.top - margin.bottom;

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

    data.forEach(function(d) {
      d.date = parseTime(d.date);
      d.value = +d.value;
    });

    function sortByDateAscending(a, b) {
      return a.date - b.date;
    }
    dataset = data.sort(sortByDateAscending);

    var dataGroup = d3.nest()
      .key(function(d) {
        return d.name;
      })
      .entries(data);

    dataGroup.forEach(function(d) {
      d.enabled = true;
    });
    
    var svg = d3.select('.line-chart')
      .append('svg')
      .attr('width', containerwidth)
      .attr('height', containerheight)
      .append('g')
      .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    var x = d3.scaleTime().range([0, width]),
        y = d3.scaleLinear().range([height, 0]);

    var colorScale = d3.scaleOrdinal(d3.schemeCategory10);

    var xAxis = d3.axisBottom(x).ticks(20).tickSizeInner(2).tickSizeOuter(0);
    var yAxis = d3.axisLeft(y).ticks(5).tickSizeInner(-width).tickSizeOuter(0);
    var xAxisGroup = svg.append('g').attr('class', 'x d3-axis').attr('transform', 'translate(0,' + height + ')');
    var yAxisGroup = svg.append('g').attr('class', 'y d3-axis').attr('transform', 'translate(0,0)');

    var line = d3.line()
      .x(function(d) {
        return x(d.date);
      }).y(function(d) {
        return y(d.value);
      });

    var legendItem = d3.select(".legend")
      .selectAll("li")
      .data(dataGroup)
      .enter()
      .append("li")
      .on('click', function(d) {
        d.enabled = !d.enabled;
        redraw();
      });

    legendItem
      .append("span")
      .attr("class", "color-square")
      .style("background", function(d, i) {
        return colorScale(d.key);
      });

    legendItem
      .append("span")
      .text(function(d) {
        return (d.values[0].name)
      });

    redraw();

    function redraw() {

      var fData = dataGroup.filter(function(d) {
        return d.enabled;
      });
      
      y.domain([
        d3.min(fData, function(c) {
          return d3.min(c.values, function(d) {
            return d.value;
          });
        }),
        d3.max(fData, function(c) {
          return d3.max(c.values, function(d) {
            return d.value;
          });
        })
      ]);
      
      x.domain([
        d3.min(fData, function(c) {
          return d3.min(c.values, function(d) {
            return d.date;
          });
        }),
        d3.max(fData, function(c) {
          return d3.max(c.values, function(d) {
            return d.date;
          });
        })
      ]);
      
      xAxisGroup.call(xAxis);
      yAxisGroup.call(yAxis);

      // update selection
      lines = svg.selectAll(".d3-group")
        .data(fData, function(d) {
          return d.key
        });

      // exit the whole group
      lines
        .exit().remove();

      // enter selection
      linesEnter = lines
        .enter()
        .append("g")
        .attr("class", "d3-group");
        
      // add path on enter
      linesEnter.append("path")
        .attr("class", "d3-line");
        
      // add text on enter
      linesEnter.append("text")
        .attr("class", "my-label")
        .attr("x", function(d,i){
          return i * 100;
        })
        .attr("y", 10);
        
      // update + enter
      lines = lines.merge(linesEnter);
      
      // adjust label
      lines.select(".my-label")
        .text(function(d,i){
          return "hi Mom " + d.key;
        });

      // adjust path
      lines.select(".d3-line")
        .attr('d', function(d) {
          return line(d.values);
        })
        .style("stroke", function(d, i) {
          return colorScale(d.key);
        });

    }
  </script>
</body>

</html>

查看更多
登录 后发表回答