Парциальные силы на узлах в 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 ответа

Решение

Чтобы применить силу только к подмножеству узлов, вам в основном нужны опции:

  1. Реализуйте свою собственную силу, которая не так сложна, как может показаться, потому что

    Сила - это просто функция, которая изменяет положение узлов или скорости;

- или, если вы хотите придерживаться стандартных сил -

  1. Создать стандартную силу и перезаписать ее 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);
});
Другие вопросы по тегам