矩形容器内的边界圆节点

时间:2018-08-12 03:47:17

标签: javascript d3.js bar-chart data-visualization force-layout

前几天,我一直在解决这个问题,但不幸的是还没有找到解决方案。我正在尝试实现here所示的行为,但通常是根据每个节点的属性针对各种容器执行此操作。我想伸出手来,问一般是否有已知的方法。

下面是我的JSFiddle,它是我目前拥有的一个示例-分配给随机组号的节点数和一个barView函数,该函数根据节点的组将这些节点分开。我希望将这些节点限制在其各自条形图的尺寸之内,以使拖动这些节点不能将其从其框中移除,但它们可以在其中移动(彼此反弹)。非常感谢您的帮助。

为简单起见,我在每个节点中制作了与“总计”字段相关的条形(以显示SVG尺寸中的条形),但是这些与我的实现中的大小相关,类似于体积。

我已经能够通过使用以下代码来组织节点的x位置,其中位置基于组:

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

使用此代码,我不确定如何在矩形的尺寸范围内工作,或保持圆可以在其中反弹的边界。非常感谢您的帮助!

非常感谢您!

我的小提琴:http://jsfiddle.net/abf2er7z/2/

1 个答案:

答案 0 :(得分:2)

一种可能的解决方案是设置一个新的tick函数,该函数使用Math.maxMath.min来获取这些矩形的边界:

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);
        });
});

这是演示:

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>

请记住,这不是最终解决方案,而只是一般指导:过渡,数学(带有那些魔术数字)和小数位数需要改进。