d3 Click to create circle and click to remove

2019-02-26 23:31发布

问题:

I am trying to click on a circle to remove it, but clicking on the canvas will create a circle.

I do want to actually remove the circle and it's object from the data, as opposed to just making it transparent.

On click on a circle calls the function

  function removeElement(d) {
    d3.select(this)
      .remove();
  }

This function is called with a basic click on the circle,

  .on("click", removeElement);

I think I'm not correctly distinguishing between clicking on the canvas to create a circle where none exists and clicking on an existing circle to remove it.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  .active {
    stroke: #000;
    stroke-width: 2px;
  }
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
  var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    radius = 32;

  var data = [{
      x: 100,
      y: 200
    },
    {
      x: 200,
      y: 300
    },
    {
      x: 300,
      y: 200
    },
    {
      x: 400,
      y: 300
    }
  ];

  var xScale = d3.scaleLinear()
    .domain([0, d3.max(data, function(d) {
      return d.x_pos
    })]).range([0, width]);

  svg.selectAll("circle")
    .data(data)
    .enter().append("circle")
    .attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    })
    .attr("r", radius)
    .style("fill", "lightblue")
    .attr('id', function(d, i) {
      return 'rect_' + i;
    })
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended))
    .on("click", removeElement);

  svg.on("click", function() {
    var coords = d3.mouse(this);

    var newData = {
      x: d3.event.x,
      y: d3.event.y
    };

    data.push(newData);

    svg.selectAll("circle") // For new circle, go through the update process
      .data(data)
      .enter()
      .append("circle")
      .attr("cx", function(d) {
        return d.x;
      })
      .attr("cy", function(d) {
        return d.y;
      })
      .attr("r", radius)
      .style("fill", "red")
      .attr('id', function(d, i) {
        return 'circle_' + i;
      })
      .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended))
      .on("click", removeElement);
  })

  function dragstarted(d) {
    d3.select(this).raise().classed("active", true);
  }

  function dragged(d) {
    d3.select(this)
      .attr("cx", d.x = d3.event.x)
      .attr("cy", d.y = d3.event.y);
  }

  function dragended(d) {
    d3.select(this)
      .classed("active", false);
  }

  function removeElement(d) {
    // need to remove this object from data
    d3.select(this)
      .remove();
  }
</script>

This question is an extension of the question D3 remove circle on dbclick

Thanks,

回答1:

The problem with your code right now is that, if you click on a blue circle, it will both delete that circle and create a new, red circle. That happens because the event bubbles up to the SVG parent element.

An easy solution is doing this inside the removeElement function:

d3.event.stopPropagation();

As the documentation explains, stopPropagation...

... prevents further propagation of the current event in the capturing and bubbling phases.

Here is the code with that change:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  .active {
    stroke: #000;
    stroke-width: 2px;
  }
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
  var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    radius = 32;

  var data = [{
      x: 100,
      y: 200
    },
    {
      x: 200,
      y: 300
    },
    {
      x: 300,
      y: 200
    },
    {
      x: 400,
      y: 300
    }
  ];

  var xScale = d3.scaleLinear()
    .domain([0, d3.max(data, function(d) {
      return d.x_pos
    })]).range([0, width]);

  svg.selectAll("circle")
    .data(data)
    .enter().append("circle")
    .attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    })
    .attr("r", radius)
    .style("fill", "lightblue")
    .attr('id', function(d, i) {
      return 'rect_' + i;
    })
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended))
    .on("click", removeElement);

  svg.on("click", function() {
    var coords = d3.mouse(this);

    var newData = {
      x: d3.event.x,
      y: d3.event.y
    };

    data.push(newData);

    svg.selectAll("circle") // For new circle, go through the update process
      .data(data)
      .enter()
      .append("circle")
      .attr("cx", function(d) {
        return d.x;
      })
      .attr("cy", function(d) {
        return d.y;
      })
      .attr("r", radius)
      .style("fill", "red")
      .attr('id', function(d, i) {
        return 'circle_' + i;
      })
      .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended))
      .on("click", removeElement);
  })

  function dragstarted(d) {
    d3.select(this).raise().classed("active", true);
  }

  function dragged(d) {
    d3.select(this)
      .attr("cx", d.x = d3.event.x)
      .attr("cy", d.y = d3.event.y);
  }

  function dragended(d) {
    d3.select(this)
      .classed("active", false);
  }

  function removeElement(d) {
    d3.event.stopPropagation();
    // need to remove this object from data
    d3.select(this)
      .remove();
  }
</script>

PS: As you probably know by now, I'm not a big fan of answering different problems in the same question (which is something that I always advise OPs to avoid). Therefore, I'm not dealing with the data issue, that is, removing the data associated with the clicked element. You can always ask another question... it's free!