D3 drag & click in v6 for grouped objects

Viewed 2552

Working off of Mike's example I'm trying to figure out how to transfer the concept to grouped elements. I can create initiate drag and click individually, but together I can't seem to get click to work. Below is a snippet - this is just one of many, many approaches I've used.

  const width = 400;
  const height = 300;
  const radius = 5;
  
  const svg = d3.select("#chart").append('svg')
    .attr('width', '400')
    .attr('height', '400')
    .style('border', 'solid 1px');

  const circles = d3.range(20).map(i => ({
    x: Math.random() * (width - radius * 2) + radius,
    y: Math.random() * (height - radius * 2) + radius,
    index: i
  }));

const group =  svg.selectAll("g").data(circles).enter()
      .append("g")
      .attr("cx", d => d.x)
      .attr("cy", d => d.y)            
      .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended))
  
  group.append("circle")
      .attr("cx", d => d.x)
      .attr("cy", d => d.y)            
      .attr("r", radius)
      .attr("fill", d => d3.schemeCategory10[d.index % 10])
      .on("click", clicked);
      
    group.append("text")
      .attr("x", d => d.x)
      .attr("y", d => d.y)            
      .style("fill", 'white')
      .text('a')
    

  function clicked(event, d) {
    if (event.defaultPrevented) return; // dragged

    d3.select(this).transition()
        .attr("fill", "black")
        .attr("r", radius * 2)
      .transition()
        .attr("r", radius)
        .attr("fill", d3.schemeCategory10[d.index % 10]);
  }


  function dragstarted() {
    d3.select(this).attr("stroke", "black");
  }

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

  function dragended() {
    d3.select(this).attr("stroke", null);
  }
<script src="https://d3js.org/d3.v6.js"></script>
<div id="chart"></div>

1 Answers

The key issue is you are positioning each g with cx, cy attributes, and updating these on drag. g elements can't be positioned by cx or cy, so setting these initially and updating them on drag will result in no change.

You'll need to position the g elements with a translate:

  .attr("transform", d=> "translate("+[d.x,d.y]+")" )   

Also, you don't need to position the circles and the text now - or every x and y coordinate will be applied twice - once to the g and once to the child element.

Now you only need to update the translate every drag, something like:

function drag(event,d) {
    d.x = event.x;
    d.y = event.y;
    d3.select(this).raise().attr("transform", d=> "translate("+[d.x,d.y]+")" )
}

Which should give you something like:

const width = 400;
  const height = 300;
  const radius = 5;
  
  const svg = d3.select("#chart").append('svg')
    .attr('width', '400')
    .attr('height', '400')
    .style('border', 'solid 1px');

  const circles = d3.range(20).map(i => ({
    x: Math.random() * (width - radius * 2) + radius,
    y: Math.random() * (height - radius * 2) + radius,
    index: i
  }));

const group =  svg.selectAll("g").data(circles).enter()
      .append("g")
      .attr("transform", d=> "translate("+[d.x,d.y]+")" )            
      .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended))
  
  group.append("circle")    
      .attr("r", radius)
      .attr("fill", d => d3.schemeCategory10[d.index % 10])
      .on("click", clicked);
      
    group.append("text")    
      .style("fill", 'white')
      .text('a')
    

  function clicked(event, d) {
    if (event.defaultPrevented) return; // dragged

    d3.select(this).transition()
        .attr("fill", "black")
        .attr("r", radius * 2)
      .transition()
        .attr("r", radius)
        .attr("fill", d3.schemeCategory10[d.index % 10]);
  }


  function dragstarted() {
    d3.select(this).attr("stroke", "black");
  }

  function dragged(event, d) {
    d.x = event.x;
    d.y = event.y;
    d3.select(this).raise().attr("transform", d=> "translate("+[d.x,d.y]+")" )
  }

  function dragended() {
    d3.select(this).attr("stroke", null);
  }
<script src="https://d3js.org/d3.v6.js"></script>
<div id="chart"></div>

Related