【问题标题】:How do I create a custom force that constrains nodes to a specific area within an SVG?如何创建将节点约束到 SVG 中特定区域的自定义力?
【发布时间】:2020-01-21 15:59:26
【问题描述】:

我正在创建一个力导向布局,我想将一系列圆圈分成三个不同的组。然后每个组都有一个对应的中心点。

到目前为止一切顺利。现在我想将每组中的圆圈限制为一种边界框。请看下图。

我认为要走的路是使用自定义力量。这是我需要一些帮助的地方。有没有人能够做到这一点?

这是我目前所拥有的:

.force('custom_1', (alpha) => {
    for(let node of data) { 
       let centerX = groupCenterPoints[node.group].x;
       let minX = centerX - ( (w/3)/2 );
       let maX = centerX + ( (w/3)/2 );

       //not sure how to modify node.vx here?
    }
});

function createData(max) {
  let data = [];
  
  for(let i = 0 ; i < max; i++) {
     data.push({
      group: chance.character({ pool: 'abc' }),
      r: 10
     });
  }
  
  return data;
}

let data = createData(150);

let svg = d3.select('#container');

let w = parseInt( svg.style('width') );
let h = parseInt( svg.style('height') );

let groupCenterPoints = {
  a: {x: ((w/3)/2) + (w/3 * 0), y: h/2},
  b: {x: ((w/3)/2) + (w/3 * 1), y: h/2},
  c: {x: ((w/3)/2) + (w/3 * 2), y: h/2}
}

let nodes = svg.selectAll('.nodes')
  .data(data)
  .enter()
  .append('circle')
  .attr('r', (d) => { return d.r; })
  .attr('fill', 'none')
  .attr('stroke', 'black');
  
 
let simulation = d3.forceSimulation()
			.force('x', d3.forceX((d) => { return groupCenterPoints[d.group].x }))
			.force('y', d3.forceY((d) => { return groupCenterPoints[d.group].y }))
			.force('collision', d3.forceCollide().radius((d) => { return d.r + 2 }))
      .force('custom_1', (alpha) => {
        for(let node of data) {
        
          let centerX = groupCenterPoints[node.group].x;
          let minX = centerX - ( (w/3)/2 );
          let maX = centerX + ( (w/3)/2 );
          
          //not sure how to modify node.vx here?
        }
      });
      
      
simulation
			.nodes(data)
			.on('tick', () => {
				nodes.attr('cx', (d) => { return d.x; })
        nodes.attr('cy', (d) => { return d.y; })
			});
#container {
  width:100vw;
  height: 100vh;
    
  margin: 0;
  padding: 0;
}
<script src="https://chancejs.com/chance.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<svg id="container"></svg>

【问题讨论】:

  • 作为一个实际问题,我想问您为什么要编写自己的代码,而不是使用已经让您编写支持 SVG 的强制定向布局的几个 JS 图形库之一。然后作为后续行动,我的第一个想法可能是“看看其中一个是如何做到的,因为它们都是开源的”?
  • 我还没有使用武力。但作为一种不同的策略,您也许可以将数据拆分为包含在三个不同的 &lt;svg&gt; 容器中,并使用基本 CSS 将它们分开

标签: javascript d3.js d3-force-directed


【解决方案1】:

这是针对类似可视化需求的解决方案,但功能需求略有不同。

在这里,节点被分组到不同的“区域”中,就像你的情况一样。然后要求用户将节点从一个区域拖到另一个区域,但是基于节点和区域特征的某些区域被禁用或“死”。

它使用标准力

that.simulation = this.d3.forceSimulation()
        .force('collide',d3.forceCollide()
                           .radius(d => {
                             return d.type === 'count' ? 60 : 30
                           }))
        // .force('charge',d3.forceManyBody()
        //                    .strength(10))
        .on('tick',ticked)

下面的代码是dragged 处理程序,它将节点限制在死区。

没有完全调试,自 10 月以来一直没有动过,但希望它会有所帮助。

在被拖动的处理程序下方是ticked 函数,它在初始位置和拖动后定位元素。

拖动

关键部分大约在下半场,在这些 cmets 之后:

      // positioning of dragged node under cursor
      // respecting all deadzone and perimiter boundaries and node radius
// drag in progress handler
    // d = the d3 object associated to the dragged circle
    function dragged(d) {
      that.trace()
      let debugcoord = [10,20]
      let r  = RAD_KEY + 4
      let dx = d3.event.dx
      let dy = d3.event.dy
      let fx = d.fx
      let fy = d.fy
      let nx = fx+dx
      let ny = fy+dy

      // DEBUGGING CODE BELOW, DO NOT DELETE
      // let curX = Math.round(d3.event.sourceEvent.clientX-that.g.node().getClientRects()[0].left)
      // let curY = Math.round(d3.event.sourceEvent.clientY-that.g.node().getClientRects()[0].top)
      // that.g.selectAll('text.coordinates').remove()
      // that.g
      // .append('text')
      // .classed('coordinates',true)
      // .attr('x',debugcoord[0])
      // .attr('y',debugcoord[1])
      // .text(`${curX},${curY}`)

      // check out this fx,fy description for reference:
      //   https://stackoverflow.com/a/51548821/4256677
      // deadzones are the inverse of livezones
      let deadzone = []
      // the names of all valid transitions, used to calculate livezones
      let trans    = !!d.trans ? d.trans.map(t => t.to.name) : []
      // the name value of the workflow status of the dragged node
      let name     = d.name
      // all the zones:
      let zones    = d3.selectAll('g.zone.group > rect.zone')
      // the maximum x+width value of all nodes in 'zones' array
      let right    = d3.max(zones.data(), n => parseFloat(n.x)+parseFloat(n.width))
      // the maximum y+height value of all nodes in 'zones' array
      let bottom   = d3.max(zones.data(), n => parseFloat(n.y)+parseFloat(n.height))

      // the zones which represent valid future states to transition
      // the dragged node (issue)
      let livezones = zones.filter(function(z,i,nodes) {
        // the current zone object
        let zone     = d3.select(this)
        // the name of the current zone object
        let zonename = zone.attr('data-zone')
        // boolean referring to the current zone representing a valid future
        // state for the node
        let isLive = trans.includes(zonename) || name == zonename
        // deadzone recognition and caching
        if(!isLive)
        {
          let coords = {name:zonename,
                        x1:parseFloat(z.x),
                        x2:parseFloat(z.x)+parseFloat(z.width),
                        y1:parseFloat(z.y),
                        y2:parseFloat(z.y)+parseFloat(z.height)}
          deadzone.push(coords)
        }
        return isLive
      }).classed('live',true) // css for livezones
      d3.selectAll('rect.zone:not(.live)').classed('dead',true) // css for deadzones

      // positioning of dragged node under cursor
      // respecting all deadzone and perimiter boundaries and node radius
      that.nodes.filter(function(d) { return d.dragging; })
      .each(function(d) {
        if(deadzone.length > 0)
        {
          d.fx += deadzone.reduce((a,c) => {
            a =
                // node is in graph
                (nx > 0 + r && nx < right - r)
                // deadzone is in left column and node is to the right or above or below
                && ((c.x1 == 0 && (nx > c.x2 + r || ny < c.y1 - r || ny > c.y2 + r))
                    // or deadzone is in the right column and node is to the left, above or below
                    || (c.x2 == right && (nx < c.x1 - r || ny < c.y1 - r || ny > c.y2 + r))
                    // or deadzone is not in left column and node is to the left, right, above or below
                    || (c.x1 > 0 && (nx < c.x1 - r || nx > c.x2 + r || ny < c.y1 - r || ny > c.y2 + r))
                  )
                ? dx : 0
            return a
          },0)
          d.fy += deadzone.reduce((a,c) => {
            a =
                // node is in graph
                (ny > 0 + r && ny < bottom - r)
                // deadzone is in top row and node is below or to the left or right
                && ((c.y1 == 0 && (ny > c.y2 + r || nx < c.x1 - r || nx > c.x2 + r))
                    // or deadzone is in the right column and node is to the left, above or below
                    || (c.y2 == bottom && (ny < c.y1 - r || nx < c.x1 - r || nx > c.x2 + r))
                    // or deadzone is not in top row and node is above, below, left or right
                    || (c.y1 > 0 && (ny < c.y1 - r || ny > c.y2 + r || nx < c.x1 - r || nx > c.x2 + r))
                  )
                ? dy : 0

            // DEBUGGING CODE BELOW, DO NOT DELETE
            // that.g
            // .append('text')
            // .classed('coordinates',true)
            // .attr('x',debugcoord[0])
            // .attr('y',debugcoord[1]+25)
            // .text(`${Math.round(nx)},${Math.round(ny)} vs ${r},${r},${right-r},${bottom-r}`)
            // that.g
            // .append('text')
            // .classed('coordinates',true)
            // .attr('x',10)
            // .attr('x',debugcoord[0])
            // .attr('y',debugcoord[1]+50)
            // .text(`dz coords: ${c.x1},${c.y1} ${c.x2},${c.y2}`)
            // that.g
            // .append('text')
            // .classed('coordinates',true)
            // .attr('x',debugcoord[0])
            // .attr('y',debugcoord[1]+75)
            // .text(c.name)
            return a
          },0)
        }
        else
        {
          d.fx += dx
          d.fy += dy
        }

      })
    }

勾选

关键部分在评论后// if were no longer dragging

function ticked(e) {

          if(!!that.links && that.links.length > 0)
          {
            that.links
            .attr("x1", function(d) { return d.source.x; })
            .attr("y1", function(d) { return d.source.y; })
            .attr("x2", function(d) { return d.target.x; })
            .attr("y2", function(d) { return d.target.y; })
          }

          that.nodes
            .attr("cx", function(d) { return d.x; })
            .attr("cy", function(d) { return d.y; })
            .each(function(d) {
              if(typeof d.selected === 'undefined')
                d.selected = false
              if(typeof d.previouslySelected === 'undefined')
                d.previouslySelected = false
            })

          that.labels
            .attr("x", function(d) { return d.x })
            .attr("y", function(d) {
              return d.type === 'count' ? d.y+6 : d.y+4
            })
            .attr('class',(d) => { return that.getClassFromNodeName(d.name)})
            .classed('count', (d) => {
              return d.type === 'count' ? true : false
            })

          // if were no longer dragging
          if(!that.dragging)
          {
            let k = 4*this.alpha()

            that.nodes.each(function(n,i) {
              let zclass = that.getClassFromNodeName(n.name)
              let z = that.zones[zclass]
              n.x += (z.x + z.width/2 - n.x) * k
              n.y += (z.y + z.height/2 - n.y) * k
            })
          }
          that.nodes
            // .each(pos)
            .attr('cx',d => { return d.x }) //boundary(d,'x')})
            .attr('cy',d => { return d.y }) //boundary(d,'y')})
          that.labels
            // .each(pos)
            .attr('x',d => { return d.x }) //boundary(d,'x')})
            .attr('y',d => { return d.y + (d.type === 'count'?6:4) }) //boundary(d,'y') + (d.type === 'count'?6:4)})
        }

【讨论】:

    猜你喜欢
    • 2013-05-22
    • 1970-01-01
    • 2019-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-10
    • 1970-01-01
    相关资源
    最近更新 更多