首页 > 解决方案 > d3 中的跳跃式转换强制有向图

问题描述

我在 d3 中有一个力有向图,我想在多个重叠的数据集之间进行转换。我希望节点顺利过渡,但我在故障排除方面遇到了困难。我试图密切关注Bostock 的示例,但我的节点在没有任何过渡的情况下跳跃。我用夸张的缓慢过渡做了一个例子来展示正在发生的事情。保留在图中的节点会立即跳转到它们的新位置并在模拟开始时冻结。这是我的。以下是相关代码:

const cb = {"edges":[{"source":"a","target":"b","value":1},{"source":"a","target":"c","value":1},{"source":"b","target":"c","value":1},{"source":"c","target":"d","value":1},                   {"source":"d","target":"e","value":1},{"source":"d","target":"f","value":1},{"source":"g","target":"h","value":1},{"source":"a","target":"g","value":1}],"nodes":[{"id":"a","pop":12.00328963067508,"size":5},{"id":"b","pop":12.391087593534877,"size":5},{"id":"c","pop":12.384324067681156,"size":5},{"id":"d","pop":13.991090521661292,"size":6},
{"id":"e","pop":13.991090521661292,"size":6},
{"id":"f","pop":13.991090521661292,"size":6},
{"id":"g","pop":13.991090521661292,"size":6},{"id":"h","pop":13.991090521661292,"size":6}]}
const sp = {"edges":[{"source":"a","target":"b","value":1},{"source":"a","target":"e","value":1},{"source":"b","target":"f","value":1},{"source":"e","target":"f","value":1}],"nodes":[{"id":"a","pop":12.00328963067508,"size":5},{"id":"b","pop":12.391087593534877,"size":5},{"id":"e","pop":13.063656176168433,"size":6},{"id":"f","pop":12.52608275807238,"size":5}]}
// set margins and canvas size
const margin = {
    top: 10,
    right: 20,
    bottom: 30,
    left: 30
};
const width = 600 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;

// set up canvas
const svg = d3.select('.chart')
    .append('svg')
    .attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom)
    .call(responsivefy)
    .append('g')
    .attr('transform', `translate(${margin.left}, ${margin.top})`);

// set up selections
let link = svg.append("g")
    .attr("class", "links")
    .selectAll("line");
let node = svg.append("g")
    .attr("class", "nodes")
    .selectAll("circle");

// set up color scale
const color = d3.scaleSequential()
    .domain([8, 15])
    .interpolator(d3.interpolateInferno);

// set up simulation basic parameters
const simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));

function render(graph) {
    // node selection and data handling
    node = node
        .data(graph.nodes, function(d) { return d.id; });

    // node general update pattern
    node.exit()
        .transition()
        .duration(1500)
        .attr("r", 0)
        .remove();
    node.transition().duration(1500)
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
    node = node
        .enter().append("circle")
        .call(function(node) { 
            node.transition().duration(1500)
              .attr("r", function(d) { return d.size; })
        })
        .attr("fill", function(d) { return color(d.pop); })
        .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended))
        .merge(node);
  
    // give all nodes a title with their id for hover identification
    node.append("title")
        .text(function(d) { return d.id; });

    // link selection, data handling
    link = link
        .data(graph.edges, function(d) { return d.source + "-" + d.target; });

    // link general update pattern with attrTween to keep links connected to disappearing nodes
    link
        .exit()
        .transition()
        .duration(1500)
        .attr("stroke-opacity", 0)
        .attr("stroke-width", 0)
        /* .attrTween("x1", function(d) {
            return function() {
                return d.source.x;
            };
        })
        .attrTween("x2", function(d) {
            return function() {
                return d.target.x;
            };
        })
        .attrTween("y1", function(d) {
            return function() {
                return d.source.y;
            };
        })
        .attrTween("y2", function(d) {
            return function() {
                return d.target.y;
            };
        }) */
        .remove();

    link = link
        .enter().append("line")
        .attr("stroke-width", function(d) { return Math.sqrt(d.value); })
        .merge(link);

    // add nodes and links to the siumlation
    simulation
        .nodes(graph.nodes)
        .on("tick", ticked);
    simulation.force("link")
        .links(graph.edges);
    // restart the simulation
    simulation.alpha(1).restart();

    // set the ticked function to constantly update node and link position
    function ticked() {
        link
            .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;
            });

        node
            .attr("cx", function(d) {
                return d.x;
            })
            .attr("cy", function(d) {
                return d.y;
            });
    }
};

// initial render
render(cb)

// dragging functions
function dragstarted(d) {
    if (!d3.event.active) simulation.alphaTarget(0.3).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.alphaTarget(0);
    d.fx = null;
    d.fy = null;
}

// responsivefy from https://brendansudol.com/writing/responsive-d3
function responsivefy(svg) {
    // get container + svg aspect ratio
    const container = d3.select(svg.node().parentNode),
        width = parseInt(svg.style("width")),
        height = parseInt(svg.style("height")),
        aspect = width / height;

    // add viewBox and preserveAspectRatio properties,
    // and call resize so that svg resizes on inital page load
    svg.attr("viewBox", "0 0 " + width + " " + height)
        .attr("preserveAspectRatio", "xMinYMid")
        .call(resize);

    // to register multiple listeners for same event type,
    // you need to add namespace, i.e., 'click.foo'
    // necessary if you call invoke this function for multiple svgs
    // api docs: https://github.com/mbostock/d3/wiki/Selections#on
    d3.select(window).on("resize." + container.attr("id"), resize);

    // get width of container and resize svg to fit it
    function resize() {
        const targetWidth = parseInt(container.style("width"));
        svg.attr("width", targetWidth);
        svg.attr("height", Math.round(targetWidth / aspect));
    }
}
<html lang="en">
<head>
<meta charset="utf-8">
<title>Updating Graph</title>
<style>
.links line {
  stroke: #999;
  stroke-opacity: 0.6;
}

.nodes circle {
  stroke: #fff;
  stroke-width: 1.5px;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
    <button onclick=render(sp)>render sp graph</button>
  <button onclick=render(cb)>render cb graph</button>
    <div class="chart"></div>
</body>
</html>

提前致谢!

标签: d3.js

解决方案


推荐阅读