Парциальные силы на узлах в D3.js
Я хочу применить несколько сил (forceX и forceY) соответственно к нескольким частям узлов.
Чтобы быть более понятным, у меня есть этот JSON в качестве данных для моих узлов:
[{
"word": "expression",
"theme": "Thème 6",
"radius": 3
}, {
"word": "théorie",
"theme": "Thème 4",
"radius": 27
}, {
"word": "relativité",
"theme": "Thème 5",
"radius": 27
}, {
"word": "renvoie",
"theme": "Thème 3",
"radius": 19
},
....
]
То, что я хочу, это применить некоторые силы исключительно к узлам, которые имеют "Thème 1" в качестве атрибута темы, или другие силы для значения "Thème 2" и т. Д...
Я искал в исходном коде, чтобы проверить, можем ли мы назначить часть узлов симуляции силе, но я не нашел ее.
Я пришел к выводу, что мне придется реализовать несколько вторичных d3.simulation()
и применять только их соответствующие части узлов, чтобы справиться с силами, которые я упоминал ранее. Вот что я думал сделать в псевдокоде d3:
mainSimulation = d3.forceSimulation()
.nodes(allNodes)
.force("force1", aD3Force() )
.force("force2", anotherD3Force() )
cluster1Simulation = d3.forceSimulation()
.nodes(allNodes.filter( d => d.theme === "Thème 1"))
.force("subForce1", forceX( .... ) )
cluster2Simulation = d3.forceSimulation()
.nodes(allNodes.filter( d => d.theme === "Thème 2"))
.force("subForce2", forceY( .... ) )
Но я думаю, что это не оптимально, учитывая вычисления.
Возможно ли применить силу к части узлов симуляции, не создавая другие симуляции?
Реализация второго решения altocumulus ':
Я попробовал это решение так:
var forceInit;
Emi.nodes.centroids.forEach( (centroid,i) => {
let forceX = d3.forceX(centroid.fx);
let forceY = d3.forceY(centroid.fy);
if (!forceInit) forceInit = forceX.initialize;
let newInit = nodes => {
forceInit(nodes.filter(n => n.theme === centroid.label));
};
forceX.initialize = newInit;
forceY.initialize = newInit;
Emi.simulation.force("X" + i, forceX);
Emi.simulation.force("Y" + i, forceY);
});
Мой массив центроидов может измениться, поэтому я должен был реализовать динамический способ реализации своих сил. Тем не менее, я получаю эту ошибку через тики симуляции:
09:51:55,996 TypeError: nodes.length is undefined
- force() d3.v4.js:10819
- tick/<() d3.v4.js:10559
- map$1.prototype.each() d3.v4.js:483
- tick() d3.v4.js:10558
- step() d3.v4.js:10545
- timerFlush() d3.v4.js:4991
- wake() d3.v4.js:5001
Я пришел к выводу, что отфильтрованный массив не присваивается узлам, и я не могу понять, почему. PS: я проверил с console.log: node.filter(...) действительно возвращает заполненный массив, так что это не источник проблемы.
2 ответа
Чтобы применить силу только к подмножеству узлов, вам в основном нужны опции:
Реализуйте свою собственную силу, которая не так сложна, как может показаться, потому что
Сила - это просто функция, которая изменяет положение узлов или скорости;
- или, если вы хотите придерживаться стандартных сил -
Создать стандартную силу и перезаписать ее
force.initialize()
метод, который будетНазначает массив узлов этой силе.
Фильтруя узлы и назначая только те, которые вас интересуют, вы можете контролировать, на какие узлы должна действовать сила:
// Custom implementation of a force applied to only every second node var pickyForce = d3.forceY(height); // Save the default initialization method var init = pickyForce.initialize; // Custom implementation of .initialize() calling the saved method with only // a subset of nodes pickyForce.initialize = function(nodes) { // Filter subset of nodes and delegate to saved initialization. init(nodes.filter(function(n,i) { return i%2; })); // Apply to every 2nd node }
Следующий фрагмент демонстрирует второй подход путем инициализацииd3.forceY
с подмножеством узлов. Из всего набора случайно распределенных кругов только каждая вторая будет иметь приложенную силу и, таким образом, будет перемещена на дно.
var width = 600;
var height = 500;
var nodes = d3.range(500).map(function() {
return {
"x": Math.random() * width,
"y": Math.random() * height
};
});
var circle = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 3)
.attr("fill", "black");
// Custom implementation of a force applied to only every second node
var pickyForce = d3.forceY(height).strength(.025);
// Save the default initialization method
var init = pickyForce.initialize;
// Custom implementation of initialize call the save method with only a subset of nodes
pickyForce.initialize = function(nodes) {
init(nodes.filter(function(n,i) { return i%2; })); // Apply to every 2nd node
}
var simulation = d3.forceSimulation()
.nodes(nodes)
.force("pickyCenter", pickyForce)
.on("tick", tick);
function tick() {
circle
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
}
<script src="https://d3js.org/d3.v4.js"></script>
Я исправил свою собственную реализацию второго решения altocumulus:
В моем случае мне пришлось создать две силы центроида. Похоже, мы не можем использовать один и тот же инициализатор для всех функций forceX и forceY.
Мне пришлось создать локальные переменные для каждого центроида в цикле:
Emi.nodes.centroids.forEach((centroid, i) => {
let forceX = d3.forceX(centroid.fx);
let forceY = d3.forceY(centroid.fy);
let forceXInit = forceX.initialize;
let forceYInit = forceY.initialize;
forceX.initialize = nodes => {
forceXInit(nodes.filter(n => n.theme === centroid.label))
};
forceY.initialize = nodes => {
forceYInit(nodes.filter(n => n.theme === centroid.label))
};
Emi.simulation.force("X" + i, forceX);
Emi.simulation.force("Y" + i, forceY);
});