1

I have been stuck on this problem from the other day, but unfortunately haven't been able to reach a solution. I am trying to achieve a behavior shown here, but in a way to generally do this for various containers depending on properties of each node. I wanted to reach out and ask if there was any known way to do this generally.

Below is my JSFiddle is an example of what I currently have - a number of nodes assigned to a random group number and a barView function which separates these nodes dependent on their groups. I hope to have these nodes be confined within the dimensions of each of their respective bars, such that dragging these nodes cannot remove them from their box, but they can move within it (bouncing off each other). I would really appreciate your help in this.

For simplicity, I made the bars related to a 'total' field in each node (to show the bars in the SVG dimensions), but these would be related to the size in my implementation, similar to a volume.

I have been able to organize the x-positions of the nodes by using the following code, where position is based on the group:

simulation.force('x', d3.forceX().strength(1).x(function(d) {
    return xscale(d.group); // xvariable
}));

With this code, I am unsure how to work within the dimensions of the rectangles, or maintain a boundary that circles can bounce within. I would appreciate your help on this!

Thank you so much!

My fiddle: http://jsfiddle.net/abf2er7z/2/

Prashant Pimpale
  • 10,349
  • 9
  • 44
  • 84
Imas
  • 177
  • 1
  • 1
  • 12

1 Answers1

3

One possible solution is setting a new tick function, which uses Math.max and Math.min to get the boundaries of those rectangles:

simulation.on("tick", function() {
    node.attr("cx", function(d) {
            return d.x = Math.min(Math.max(xscale(d.group) - 20 + d.radius, d.x), xscale(d.group) + 20 - d.radius);
        })
        .attr("cy", function(d) {
            return d.y = Math.min(Math.max(0.9 * height - heightMap[d.group] + d.radius, d.y), height - d.radius);
        });
});

Here is the demo:

var width = 900,
  height = 400;

var groupList = ['Group A', 'Group B', 'Group C', 'Group D'];
var data = d3.range(200).map(d => ({
  id: d,
  group: groupList[getRandomIntegerInRange(0, 3)],
  size: getRandomIntegerInRange(1, 100),
  total: getRandomIntegerInRange(1, 10)
}))

var svg = d3.select("body")
  .append("svg")
  .attr("viewBox", "0 0 " + (width) + " " + (height))
  .attr("preserveAspectRatio", "xMidYMid meet")
  .attr('width', "100%")
  .attr('height', height)
  .attr('id', 'svg')
  .append('g')
  .attr('id', 'container')
  .attr('transform', 'translate(' + 0 + ', ' + 0 + ')');

simulation = d3.forceSimulation();

data.forEach(function(d, i) {
  d.radius = Math.sqrt(d['size']);
});

colorScale = d3.scaleOrdinal(d3.schemeCategory10);

node = svg.append("g")
  .attr("class", "node")
  .selectAll(".bubble")
  .data(data, function(d) {
    return d.id;
  })
  .enter().append("circle")
  .attr('class', 'bubble')
  .attr('r', function(d) {
    return d.radius;
  }) // INITIALIZED RADII TO 0 HERE
  .attr("fill", function(d) {
    // initially sets node colors
    return colorScale(d.group);
  })
  .attr('stroke-width', 0.5)
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

function dragstarted(d) {
  if (!d3.event.active) {
    simulation.alpha(.07).restart()
  }

  d.fx = d.x;
  d.fy = d.y;
}

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  if (!d3.event.active) simulation.alpha(0.07).restart()

  d.fx = null;
  d.fy = null;
  // Update and restart the simulation.
  simulation.nodes(data);
}

simulation
  .nodes(data)
  .force("x", d3.forceX().strength(0.1).x(width / 2))
  .force("y", d3.forceY().strength(0.1).y(height / 2))
  .force("collide", d3.forceCollide().strength(0.7).radius(function(d) {
    return d.radius + 0.5;
  }).iterations(2))
  .on("tick", function() {
    node
      .attr("cx", function(d) {
        return d.x;
      })
      .attr("cy", function(d) {
        return d.y;
      });
  });

function barView() {
  var buff = width * 0.12

  var leftBuff = buff;
  var rightBuff = width - buff;

  var scale;

  xscale = d3.scalePoint()
    .padding(0.1)
    .domain(groupList)
    .range([leftBuff, rightBuff]);


  // Save double computation below.
  heightMap = {}
  groupList.forEach(function(d) {
    currVarTotal = data.filter(function(n) {
      return n.group === d;
    }).reduce(function(a, b) {
      return a + +b.total;
    }, 0);
    heightMap[d] = currVarTotal;
  })


  var rects = svg.selectAll('.rect')
    .data(groupList)
    .enter()
    .append('rect')
    .attr('x', function(d) {
      return xscale(d) - 20
    })
    .attr('y', function(d) {
      return 0.9 * height - heightMap[d];
    })
    .attr('width', 40)
    .attr('height', function(d) {
      return heightMap[d];
    })
    .attr('fill', 'transparent')
    .attr('stroke', function(d) {
      return colorScale(d)
    })
    .attr('stroke-width', 2)
    .attr('class', 'chartbars');

  drawTheAxis(xscale);

  simulation.force('x', d3.forceX().strength(1).x(function(d) {
    return xscale(d.group); // xvariable
  })).on("tick", function() {
    node
      .attr("cx", function(d) {
        return d.x = Math.min(Math.max(xscale(d.group) - 20 + d.radius, d.x), xscale(d.group) + 20 - d.radius);
      })
      .attr("cy", function(d) {
        return d.y = Math.min(Math.max(0.9 * height - heightMap[d.group] + d.radius, d.y), height - d.radius);
      });
  });

  currHeights = {}
  Object.keys(heightMap).forEach(d => {
    currHeights[d] = 0.9 * height
  });

  // restart the simulation
  simulation.alpha(0.07).restart();

  function drawTheAxis(scale) {

    var bottomBuffer = 0.9 * height;
    // create axis objects
    var xAxis = d3.axisBottom(xscale);

    // Draw Axis
    var gX = svg.append("g") // old: nodeG.append
      .attr("class", "xaxis")
      .attr('stroke-width', 2)
      .attr("transform", "translate(0," + height + ")")
      .attr('opacity', 0)
      .call(xAxis)
      .transition()
      .duration(250)
      .attr('opacity', 1)
      .attr("transform", "translate(0," + bottomBuffer + ")");
  }
}

function getRandomIntegerInRange(min, max) {
  return Math.floor(Math.random() * (Math.floor(max) - Math.ceil(min) + 1)) + Math.ceil(min);
}

setTimeout(function() {
  barView();
}, 1500);
<script src="https://d3js.org/d3.v5.min.js"></script>

Have in mind that this is not a final solution, but just a general guidance: the transition, the math (with those magic numbers) and the scales need to be improved.

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171