Можно ли указать пользовательскую функцию силы для макета с направлением силы?
Я хочу поэкспериментировать с альтернативными семейными силовыми функциями для макетов графов, ориентированных на силу.
Для каждого узла n_i
Я могу определить "функцию силы" f_i
такой, что
f_i ( n_i )
тождественно ноль; а такжеf_i ( n_j )
, гдеn_i != n_j
, это сила на узлеn_i
это связано с каким-то другим узломn_j
,
Чистая сила на узле n_i
тогда должна быть векторная сумма сил f_i ( n_j )
, где n_j
распространяется на все остальные узлы 1.
Есть ли какой-нибудь способ указать d3.js использовать эти пользовательские функции форсирования в алгоритме компоновки?
[ Документация к силовому макету d3.js описывает различные способы настройки его встроенной функции силы, но я не смог найти способ полностью указать совершенно другую функцию силы, то есть функцию силы, которая не может быть достигнуто путем настройки параметров встроенной функции силы.]
1 IOW, никакие другие / дополнительные силы не должны действовать на узел n_i
кроме тех, которые рассчитаны по его силовой функции f_i
,
2 ответа
Для этого вам нужно создать свой собственный макет. Я не знаю учебника по этому вопросу, но исходный код для существующего макета Force должен быть хорошей отправной точкой, так как, по звучанию, структура вашего пользовательского макета будет очень похожа на эту.
Да, ты можешь. Кредит идет к Шен Картер и его пример.
let margin = {
top: 100,
right: 100,
bottom: 100,
left: 100
};
let width = 960,
height = 500,
padding = 1.5, // separation between same-color circles
clusterPadding = 6, // separation between different-color circles
maxRadius = 12;
let n = 200, // total number of nodes
m = 10, // number of distinct clusters
z = d3.scaleOrdinal(d3.schemeCategory20),
clusters = new Array(m);
let svg = d3.select('body')
.append('svg')
.attr('height', height)
.attr('width', width)
.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
let nodes = d3.range(200).map(() => {
let i = Math.floor(Math.random() * m),
radius = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
r: radius
};
if (!clusters[i] || (radius > clusters[i].r)) clusters[i] = d;
return d;
});
let circles = svg.append('g')
.datum(nodes)
.selectAll('.circle')
.data(d => d)
.enter().append('circle')
.attr('r', (d) => d.r)
.attr('fill', (d) => z(d.cluster))
.attr('stroke', 'black')
.attr('stroke-width', 1);
let simulation = d3.forceSimulation(nodes)
.velocityDecay(0.2)
.force("x", d3.forceX().strength(.0005))
.force("y", d3.forceY().strength(.0005))
.force("collide", collide) // <<-------- CUSTOM FORCE
.force("cluster", clustering)//<<------- CUSTOM FORCE
.on("tick", ticked);
function ticked() {
circles
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);
}
// Custom 'clustering' force implementation.
function clustering(alpha) {
nodes.forEach(function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.r + cluster.r;
if (l !== r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
});
}
// Custom 'collide' force implementation.
function collide(alpha) {
var quadtree = d3.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.addAll(nodes);
nodes.forEach(function(d) {
var r = d.r + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.data && (quad.data !== d)) {
var x = d.x - quad.data.x,
y = d.y - quad.data.y,
l = Math.sqrt(x * x + y * y),
r = d.r + quad.data.r + (d.cluster === quad.data.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.data.x += x;
quad.data.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
});
}
<!doctype html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
Также здесь более углубленный взгляд на предмет.