拖动节点时如何使 d3js 力有向图不那么不稳定?
How to make d3js force directed graph less shaky when a node is dragged?
我希望我的力导向图在我将一个节点拖到另一点时保持平静。此时,将一个小节点拖离中心较远,会导致整个图不受控制地晃动。这可能是一团糟,因为节点很多(超过 100 个)...
这是描述问题的短视频:https://gfycat.com/GleamingMellowHypacrosaurus
我知道setting the coordinate of all the nodes的方法,但是不行,因为节点太多,以后可能会增加。
这是我的代码:
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, '.');
}
if (str[1] && str[1].length >= 5) {
str[1] = str[1].replace(/(\d{3})/g, ' ');
}
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)
这是预期的行为,因为您正在重新启动模拟:
simulation.alphaTarget(0.7).restart()
此外,这一行应该在"start"监听器中,而不是在"drag"监听器中: 每秒重新启动模拟几次是没有意义的,你明白吗?
回到问题:你想要的结果("I want my force directed graph to stay calm when I drag one node")不是很清楚。什么是"stay calm"?但是,如果我没理解错的话,你可以简单地修复所有其他节点。
由于您没有提供 运行 代码,这里是使用 this example by Mike Bostock 的演示。我在这里所做的只是:
node.each(function(d){
d.fx = d.x;
d.fy = d.y;
})
这里是修改后的bl.ocks:https://bl.ocks.org/anonymous/93c0c9af8c729b62b1b194841298bc49/ede207278c873aa311133782e9fb70e7504ed622
我希望我的力导向图在我将一个节点拖到另一点时保持平静。此时,将一个小节点拖离中心较远,会导致整个图不受控制地晃动。这可能是一团糟,因为节点很多(超过 100 个)...
这是描述问题的短视频:https://gfycat.com/GleamingMellowHypacrosaurus
我知道setting the coordinate of all the nodes的方法,但是不行,因为节点太多,以后可能会增加。
这是我的代码:
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, '.');
}
if (str[1] && str[1].length >= 5) {
str[1] = str[1].replace(/(\d{3})/g, ' ');
}
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)
这是预期的行为,因为您正在重新启动模拟:
simulation.alphaTarget(0.7).restart()
此外,这一行应该在"start"监听器中,而不是在"drag"监听器中: 每秒重新启动模拟几次是没有意义的,你明白吗?
回到问题:你想要的结果("I want my force directed graph to stay calm when I drag one node")不是很清楚。什么是"stay calm"?但是,如果我没理解错的话,你可以简单地修复所有其他节点。
由于您没有提供 运行 代码,这里是使用 this example by Mike Bostock 的演示。我在这里所做的只是:
node.each(function(d){
d.fx = d.x;
d.fy = d.y;
})
这里是修改后的bl.ocks:https://bl.ocks.org/anonymous/93c0c9af8c729b62b1b194841298bc49/ede207278c873aa311133782e9fb70e7504ed622