了解 D3 Force-Layout:为什么在 1 个时间步之后先前的节点值 x、y 不等于 px、py?

Understanding D3 Force-Layout: Why previous node values x,y NOT EQUAL px,py after 1 timestep?

我从 运行 到 Stephen A. Thomas's D3 Force Layout tutorial,通常边玩边玩每一个,进行修改,并试图更深入地了解部队布局。在示例 3 中,我注意到有些事情看起来有点奇怪,希望有人能解释为什么会这样。

我做了修改(见下面的片段),通过绘图绘制节点从初始位置到最终位置时所采用的路径每个报价期间从 pxx 的一行。

由于某些原因,节点最后一个刻度的实际位置与当前刻度期间列出的先前(px,py)位置不相等;或者,node.x(t), node.y(t) 不等于 node.px(t+1), node.py(t+1)...

第一步:

第二步:

路径中的间隙显示错误:

我确信在几乎所有用例中这并不重要,但本着理解强制布局如何工作的精神,有人可以解释这里发生了什么吗?它是否特定于我(或教程)逐笔报价的方式,如果是,为什么?

// Define the dimensions of the visualization. We're using
// a size that's convenient for displaying the graphic on
// http://jsDataV.is

var width = 1600,
    height = 900;
  
// Before we do anything else, let's define the data for the visualization.

var graph = {
    "nodes": [  { "x": 208.992345, "y": 273.053211 },
                { "x": 595.98896,  "y":  56.377057 },
                { "x": 319.568434, "y": 278.523637 },
                { "x": 214.494264, "y": 214.893585 },
                { "x": 482.664139, "y": 340.386773 },
                { "x":  84.078465, "y": 192.021902 },
                { "x": 196.952261, "y": 370.798667 },
                { "x": 107.358165, "y": 435.15643  },
                { "x": 401.168523, "y": 443.407779 },
                { "x": 508.368779, "y": 386.665811 },
                { "x": 355.93773,  "y": 460.158711 },
                { "x": 283.630624, "y":  87.898162 },
                { "x": 194.771218, "y": 436.366028 },
                { "x": 477.520013, "y": 337.547331 },
                { "x": 572.98129,  "y": 453.668459 },
                { "x": 106.717817, "y": 235.990363 },
                { "x": 265.064649, "y": 396.904945 },
                { "x": 452.719997, "y": 137.886092 }
            ],
    "links": [  { "target": 11, "source":  0 },
                { "target":  3, "source":  0 },
                { "target": 10, "source":  0 },
                { "target": 16, "source":  0 },
                { "target":  1, "source":  0 },
                { "target":  3, "source":  0 },
                { "target":  9, "source":  0 },
                { "target":  5, "source":  0 },
                { "target": 11, "source":  0 },
                { "target": 13, "source":  0 },
                { "target": 16, "source":  0 },
                { "target":  3, "source":  1 },
                { "target":  9, "source":  1 },
                { "target": 12, "source":  1 },
                { "target":  4, "source":  2 },
                { "target":  6, "source":  2 },
                { "target":  8, "source":  2 },
                { "target": 13, "source":  2 },
                { "target": 10, "source":  3 },
                { "target": 16, "source":  3 },
                { "target":  9, "source":  3 },
                { "target":  7, "source":  3 },
                { "target": 11, "source":  5 },
                { "target": 13, "source":  5 },
                { "target": 12, "source":  5 },
                { "target":  8, "source":  6 },
                { "target": 13, "source":  6 },
                { "target": 10, "source":  7 },
                { "target": 11, "source":  7 },
                { "target": 17, "source":  8 },
                { "target": 13, "source":  8 },
                { "target": 11, "source": 10 },
                { "target": 16, "source": 10 },
                { "target": 13, "source": 11 },
                { "target": 14, "source": 12 },
                { "target": 14, "source": 12 },
                { "target": 14, "source": 12 },
                { "target": 15, "source": 12 },
                { "target": 16, "source": 12 },
                { "target": 15, "source": 14 },
                { "target": 16, "source": 14 },
                { "target": 15, "source": 14 },
                { "target": 16, "source": 15 },
                { "target": 16, "source": 15 },
                { "target": 17, "source": 16 }
            ]
    };

// Here's were the code begins. We start off by creating an SVG
// container to hold the visualization. We only need to specify
// the dimensions for this container.

var svg = d3.select('div').append('svg')
    .attr('viewBox', "0 0 " + width + " " + height)
    .attr("width", "100%")

// Extract the nodes and links from the data.
var nodes = graph.nodes,
    links = graph.links;


var c = d3.scale.category20();
  
// Now we create a force layout object and define its properties.
// Those include the dimensions of the visualization and the arrays
// of nodes and links.

var force = d3.layout.force()
    .size([width, height])
    .nodes(nodes)
    .links(links);

// There's one more property of the layout we need to define,
// its `linkDistance`. That's generally a configurable value and,
// for a simple example, we'd normally leave it at its default.
// Unfortunately, the default value results in a visualization
// that's not especially clear. This parameter defines the
// distance (normally in pixels) that we'd like to have between
// nodes that are connected. (It is, thus, the length we'd
// like our links to have.)

force.linkDistance(width/4);

// Next we'll add the nodes and links to the visualization.
// Note that we're just sticking them into the SVG container
// at this point. We start with the links. The order here is
// important because we want the nodes to appear "on top of"
// the links. SVG doesn't really have a convenient equivalent
// to HTML's `z-index`; instead it relies on the order of the
// elements in the markup. By adding the nodes _after_ the
// links we ensure that nodes appear on top of links.

// Links are pretty simple. They're just SVG lines. We're going
// to position the lines according to the centers of their
// source and target nodes. You'll note that the `source`
// and `target` properties are indices into the `nodes`
// array. That's how our JSON is structured and that's how
// D3's force layout expects its inputs. As soon as the layout
// begins executing, however, it's going to replace those
// properties with references to the actual node objects
// instead of indices.

var link = svg.selectAll('.link')
    .data(links)
    .enter().append('line')
    .attr('class', 'link')
    .attr('x1', function(d) { return nodes[d.source].x; })
    .attr('y1', function(d) { return nodes[d.source].y; })
    .attr('x2', function(d) { return nodes[d.target].x; })
    .attr('y2', function(d) { return nodes[d.target].y; });

// Now it's the nodes turn. Each node is drawn as a circle and
// given a radius and initial position within the SVG container.
// As is normal with SVG circles, the position is specified by
// the `cx` and `cy` attributes, which define the center of the
// circle. We actually don't have to position the nodes to start
// off, as the force layout is going to immediately move them.
// But this makes it a little easier to see what's going on
// before we start the layout executing.

var node = svg.selectAll('.node')
    .data(nodes)
    .enter().append('circle')
    .attr('class', 'node')
    .attr('r', width/100)
    .attr('cx', function(d) { return d.x; })
    .attr('cy', function(d) { return d.y; })
  .attr("fill", function(d,i){ return c(i); });
 
var roundTo = 1;
 
var loci = svg.selectAll('.nodeLoci')
  .data(nodes)
  .enter().append('text')
  .attr('class','nodeLoci')
  .text(function(d){ return 'x: '+ d3.round(d.x,roundTo) +', y: ' + d3.round(d.y,roundTo) + '; px: '+ d3.round(d.px,roundTo) + ', py: '+ d3.round(d.py,roundTo); })
  .attr('x', width-400)
  .attr('y', function(d,i) { return 50+i*30; });
  
var trail = svg.selectAll('.trail')
  .data(nodes)
  
trail.enter().append('circle')
  .attr("cx", function(d){ return d.x; })
  .attr("cy", function(d){ return d.y; })
  .attr("r", width/200)
  .attr("fill", "black")
  .attr("fill-opacity", 0)
  .attr("stroke", function(d,i){ return c(i); })
  .attr("stroke-width", 2);

// Before we get into the force layout operation itself,
// we define a variable that indicates whether or not
// we're animating the operation. Initially it's false.

var animating = false;

// We'll also define a variable that specifies the duration
// of each animation step (in milliseconds).

var animationStep = 200;

// Next we define a function that executes at each
// iteration of the force layout.

force.on('tick', function() {
 
    // When this function executes, the force layout
    // calculations have been updated. The layout will
    // have set various properties in our nodes and
    // links objects that we can use to position them
    // within the SVG container.

    // First let's reposition the nodes. As the force
    // layout runs it updates the `x` and `y` properties
    // that define where the node should be centered.
    // To move the node, we set the appropriate SVG
    // attributes to their new values.

    // Because we want to emphasize how the nodes and
    // links move, we use a transition to move them to
    // their positions instead of simply setting the
    // values abruptly.
 
  trail.enter().append('line')
    .attr('class','trail')
    .attr("x1", function(d){ return d.px; })
    .attr("y1", function(d){ return d.py; })
    .attr("x2", function(d){ return d.x; })
    .attr("y2", function(d){ return d.y; })
    .attr("stroke", function(d,i){ return c(i); });
    
  trail.enter().append('circle')
  .attr("cx", function(d){ return d.x; })
  .attr("cy", function(d){ return d.y; })
  .attr("r", width/500)
  .attr("fill", "black")
  .attr("fill-opacity", 0)
  .attr("stroke", function(d,i){ return c(i); })
  .attr("stroke-width", 1);
    
    node.transition().ease('linear').duration(animationStep)
        .attr('cx', function(d) { return d.x; })
        .attr('cy', function(d) { return d.y; });
  
  loci.text(function(d){ return 'x: '+ d3.round(d.x,roundTo) +', y: ' + d3.round(d.y,roundTo) + '; px: '+ d3.round(d.px,roundTo) + ', py: '+ d3.round(d.py,roundTo); })
    
    // We also need to update positions of the links.
    // For those elements, the force layout sets the
    // `source` and `target` properties, specifying
    // `x` and `y` values in each case.

    // Here's where you can see how the force layout has
    // changed the `source` and `target` properties of
    // the links. Now that the layout has executed at least
    // one iteration, the indices have been replaced by
    // references to the node objects.

    link.transition().ease('linear').duration(animationStep)
        .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; });

    // We only show one tick at a time, so stop the layout
    // for now.

    force.stop();

    // If we're animating the layout, continue after
    // a delay to allow the animation to take effect.

    if (animating) {
        setTimeout(
            function() { force.start(); },
            animationStep
        );
    }

});

// Now let's take care of the user interaction controls.
// We'll add functions to respond to clicks on the individual
// buttons.

// When the user clicks on the "Advance" button, we
// start the force layout (The tick handler will stop
// the layout after one iteration.)

d3.select('#advance').on('click', force.start);

// When the user clicks on the "Play" button, we're
// going to run the force layout until it concludes.

d3.select('#slow').on('click', function() {

    // Since the buttons don't have any effect any more,
    // disable them.

    d3.selectAll('button').attr('disabled','disabled');

    // Indicate that the animation is in progress.

    animating = true;

    // Get the animation rolling

    force.start();

});
div {
  width: 1600px
}

svg {
  border-style: solid;
  border-width: 1px;
  border-color: green;
}
 
.node {
    stroke: #fff;
    stroke-width: 1px;
}

.nodeLoci {
    font-size: 1.5em;    
}

.link {
    stroke: #777;
    stroke-width: 2px;
}

button {
    position: absolute;
    width: 30px;
}
button#slow {
    margin-left: 40px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"
          rel="stylesheet">

    <button id='advance' title='Advance Layout One Increment'>
        <i class='fa fa-step-forward'></i>
    </button>
    <button id='slow'    title='Run Layout in Slow Motion'>
        <i class='fa fa-play'></i>
    </button>

<div></div>

查看 the source 在这里有所帮助。特别是第 125-126 行:

o.x -= (o.px - (o.px = o.x)) * friction;
o.y -= (o.py - (o.py = o.y)) * friction;

更新节点位置时也会考虑布局的摩擦。