简体   繁体   中英

How to make d3js force directed graph less shaky when a node is dragged?

I want my force directed graph to stay calm when I drag one node to another point. At the moment, dragging a small node far from the center can cause the whole graph to shake uncontrollably. Which could be a mess since the nodes are plenty (over 100)...

Here is a short video depicting the problem: https://gfycat.com/GleamingMellowHypacrosaurus

I know about the method of setting the coordinate of all the nodes , but it's not possible since the nodes are too many and may increase in later times.

Here is my code:

function getNeighbors(node) {
    return links.reduce(function(neighbors, link) {
        if (link.target.id === node.id) {
            neighbors.push(link.source.id)
        } else if (link.source.id === node.id) {
            neighbors.push(link.target.id)
        }
        return neighbors
    }, [node.id])
}

function isNeighborLink(node, link) {
    return link.target.id === node.id || link.source.id === node.id
}


function getNodeColor(node, neighbors) {
    // If is neighbor
    if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
        return 'rgba(251, 130, 30, 1)'
        // return node.level === 1 ? '#9C4A9C' : 'rgba(251, 130, 30, 1)'
    } else {
        // Check the node level
        if (node.level === 0) {
            return '#E72148'
        } else if (node.level === 1) {
            return '#9C4A9C'
        } else {
            return '#D8ABD8'
        }
    }
    //return node.level === 0 ? '#91007B' : '#D8ABD8'
}

function getLinkColor(node, link) {
    return isNeighborLink(node, link) ? 'rgba(251, 130, 30, .85)' : 'rgba(251, 130, 30, 0.25)'
}

function getTextColor(node, neighbors) {
    return Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1 ? '#333' : '#bbb'
}

function getLabelColor(node, link) {
    return isNeighborLink(node, link) ? 'rgba(51, 51, 51, .9)' : 'rgba(51, 51, 51, 0)'    // #333
}

var width = window.innerWidth
var height = window.innerHeight

var svg = d3.select('svg')
// svg.attr('width', width).attr('height', height)
svg.attr("width", '100%')
    .attr("height", '500px')
    .attr('viewBox', '250 0 800 600')
    //.attr('viewBox','0 0 '+Math.min(width,height)+' '+Math.min(width,height))
    .attr('preserveAspectRatio', 'xMidYMid')
    .append("g")
    .attr("transform", "translate(" + Math.min(width, height) / 2 + "," + Math.min(width, height) / 2 + ")");

//add zoom capabilities
var zoom_handler = d3.zoom()
    .scaleExtent([1 / 2, 8])
    .on("zoom", zoom_actions);

zoom_handler(svg);

function zoom_actions() {
    g.attr("transform", d3.event.transform)
}

function button_zoom_in() {
    zoom_handler.scaleBy(svg, 2);
}

function button_zoom_out() {
    zoom_handler.scaleBy(svg, 0.5);
}

// simulation setup with all forces
var linkForce = d3
    .forceLink()
    .id(function(link) {
        return link.id
    })
    // Alternative: using the distance from the data "strength"
    //.distance(50).strength(function (link) { return link.strength })
    // If don't want to use this, use default here:
    .distance(50).strength(.7)

var simulation = d3
    .forceSimulation()
    .force('link', linkForce)
    .force('charge', d3.forceManyBody().strength(-1500))
    .force('radial', d3.forceRadial(function(d) {
        return d.level * 50
    }, width / 2, height / 2))
    .force('center', d3.forceCenter(width / 2, height / 2))

var dragDrop = d3.drag().on('start', function(node) {
    node.fx = node.x
    node.fy = node.y
}).on('drag', function(node) {
    simulation.alphaTarget(0.7).restart()
    node.fx = d3.event.x
    node.fy = d3.event.y
}).on('end', function(node) {
    if (!d3.event.active) {
        simulation.alphaTarget(0)
    }
    node.fx = null
    node.fy = null
})

function selectNode(selectedNode) {
    var neighbors = getNeighbors(selectedNode)

    // we modify the styles to highlight selected nodes
    nodeElements.attr('fill', function(node) {
        return getNodeColor(node, neighbors)
    })
    textElements.attr('fill', function(node) {
        return getTextColor(node, neighbors)
    })
    linkElements.attr('stroke', function(link) {
        return getLinkColor(selectedNode, link)
    })
    labelElements.attr('fill', function(link) {
        return getLabelColor(selectedNode, link)
    }).attr("style", "-webkit-text-stroke: 1px rgba(255, 255, 255, 0.75); text-shadow: -1px -1px 0 rgba(255, 255, 255, 0.75), 1px -1px 0 rgba(255, 255, 255, 0.75), -1px 1px 0 rgba(255, 255, 255, 0.75), 1px 1px 0 rgba(255, 255, 255, 0.75)")

}

// Format the numbers to dots e.g. 100000 => 100.000
function commafy( num ) {
    var str = num.toString().split('.');
    if (str[0].length >= 5) {
        str[0] = str[0].replace(/(\d)(?=(\d{3})+$)/g, '$1.');
    }
    if (str[1] && str[1].length >= 5) {
        str[1] = str[1].replace(/(\d{3})/g, '$1 ');
    }
    return str.join('.');
}


// Enables zooming
var g = svg.append("g")
    .attr("class", "everything");
// Enables zooming end

// Create circling orbit
var circles = g.selectAll(null) // use g.selectAll instead of svg.selectAll to enable zoom
    .data([200, 350]) // sets the circle radius
    .enter()
    .append("circle")
    .attr("cx", width / 2)
    .attr("cy", height / 2)
    .attr("r", d => d)
    .style("fill", "none")
    .style("stroke", "#ddd");

var linkElements = g.append("g") // use g.append instead of svg.append to enable zoom
    .attr("class", "links")
    .selectAll("line")
    .data(links)
    .enter().append("path")
    .attr("id", function(d, i) {
        return "linkId_" + i;
    })
    .attr("stroke-width", function(link) {
        var linkValueNormalize = link.value / 100;
        var linkValueNormalize = Math.ceil(linkValueNormalize);
        if (linkValueNormalize >= 201) {
            return 26;
        } else if (linkValueNormalize >= 101 && linkValueNormalize <= 200) {
            return 20;
        } else if (linkValueNormalize >= 71 && linkValueNormalize <= 100) {
            return 16;
        } else if (linkValueNormalize >= 41 && linkValueNormalize <= 70) {
            return 12;
        } else if (linkValueNormalize >= 21 && linkValueNormalize <= 40) {
            return 8;
        } else if (linkValueNormalize >= 11 && linkValueNormalize <= 20) {
            return 12;
        } else if (linkValueNormalize >= 7 && linkValueNormalize <= 10) {
            return 8;
        } else if (linkValueNormalize >= 3 && linkValueNormalize <= 6) {
            return 4;
        } else {
            return 2;
        }
        // return linkValueNormalize;
    })
    .attr("stroke", "rgba(251, 130, 30, 0.5)")

var labelElements = g.append("g")
    .attr("class", "label")
    .selectAll("text")
    .data(links)
    .enter().append("text")
    .attr("font-size", 10)
    .attr("font-family", "sans-serif")
    .attr("fill", "rgba(51, 51, 51, 0)")  // #333
    .attr("x", "70")
    .attr("y", "-20")
    .attr("text-anchor", "start")
    .append("textPath")
    .attr("xlink:href", function(d, i) {
        return "#linkId_" + i;
    })
    .text(function(link) {
          var linkValueNormalize = link.value; 
          var linkValueNormalize = commafy(linkValueNormalize);

          return "Rp "+ linkValueNormalize +" M";

    })

var nodeElements = g.append("g") // use g.append instead of svg.append to enable zoom
    .attr("class", "nodes")
    .selectAll("circle")
    .data(nodes)
    .enter().append('a')    // Append a first, then circle
    .attr("xlink:href", function(node){return node.url;})
    .attr("target", "_blank")
    .append("circle")
    .attr("r", function(dat, index, n) {
        var linkItem = links.find(function(link) {
            return link.target == dat.id;
        });

        var radius = 26;
        var linkValueNormalize = (linkItem && linkItem.value) / 100; // in milyar
        var linkValueNormalize = Math.ceil(linkValueNormalize);

        if (linkValueNormalize >= 201) {
            radius = 24;
        } else if (linkValueNormalize >= 101 && linkValueNormalize <= 200) {
            radius = 22;
        } else if (linkValueNormalize >= 71 && linkValueNormalize <= 100) {
            radius = 18;
        } else if (linkValueNormalize >= 41 && linkValueNormalize <= 70) {
            radius = 14;
        } else if (linkValueNormalize >= 21 && linkValueNormalize <= 40) {
            radius = 10;
        } else if (linkValueNormalize >= 11 && linkValueNormalize <= 20) {
            radius = 14;
        } else if (linkValueNormalize >= 7 && linkValueNormalize <= 10) {
            radius = 10;
        } else if (linkValueNormalize <= 6) {
            radius = 6;
        }

        if (dat.level === 0) {
            radius = 26;
        }

        return radius;
    })
    .attr("fill", getNodeColor)
    .attr("stroke", "#fff")
    .attr('stroke-width', 2)
    .call(dragDrop)
    .on('mouseover', selectNode)

var textElements = g.append("g") // use g.append instead of svg.append to enable zoom
    .attr("class", "texts")
    .selectAll("text")
    .data(nodes)
    .enter().append("text")
    .text(function(node) {
        return node.label
    })
    .attr("font-size", 10)
    .attr("font-family", "sans-serif")
    .attr("text-anchor", "middle")
    .attr("fill", "#333")
    .attr("style", "font-weight:bold; -webkit-text-stroke: 1px #fff; text-shadow: 3px 3px 0 #fff, -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff")
    .attr("dx", 0)
    .attr("dy", 20)



simulation.nodes(nodes).on('tick', () => {
    nodeElements
        .attr('cx', function(node) {
            return node.x
        })
        .attr('cy', function(node) {
            return node.y
        })
    textElements
        .attr('x', function(node) {
            return node.x
        })
        .attr('y', function(node) {
            return node.y
        })
    linkElements.attr("d", function(link) {
        return "M" + link.source.x + "," + link.source.y + " L" + link.target.x + "," + link.target.y;
    });
    labelElements
        .attr('x', function(link) {
            return link.target.x
        })
        .attr('y', function(link) {
            return link.target.y
        })
})

simulation.force("link").links(links)

That's the expected behaviour, since you are restarting the simulation:

simulation.alphaTarget(0.7).restart()

Besides that, this line should be in the "start" listener, not in the "drag" listener: it makes no sense restarting the simulation several times per second, do you see?

Back to the question: your desired outcome ( "I want my force directed graph to stay calm when I drag one node" ) is not exactly clear. What is to "stay calm" ? However, if I understand it right, you can simply fix all other nodes.

Since you didn't provided a running code, here is a demo using this example by Mike Bostock . What I did here was just:

node.each(function(d){
    d.fx = d.x;
    d.fy = d.y;
})

here is the modified bl.ocks: https://bl.ocks.org/anonymous/93c0c9af8c729b62b1b194841298bc49/ede207278c873aa311133782e9fb70e7504ed622

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM