0

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

在此处输入图像描述

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

在此处输入图像描述

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

这是我到目前为止所拥有的:

.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>

4

1 回答 1

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函数,它最初定位元素,并在拖动之后定位元素。

拖着

在这些评论之后,关键部分大约下降了一半:

      // 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)})
        }
于 2020-01-22T15:51:20.560 回答