Forces partielles sur les noeuds dans D3.js

Je souhaite appliquer plusieurs forces (forceX et forceY) respectivement à plusieurs sous-parties de nœuds.

Pour être plus explicatif, j'ai ce JSON comme données pour mes noeuds:

[{ "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 }, .... ] 

Ce que je veux, c'est d'appliquer certaines forces exclusivement aux noeuds qui ont "Thème 1" comme attribut de thème, ou d'autres forces pour la valeur "Thème 2", etc.

J'ai cherché dans le code source pour vérifier si on peut attribuer une sous-partie des noeuds de la simulation à une force, mais je ne l'ai pas trouvé.

J'ai conclu que je devais mettre en œuvre plusieurs secondaires d3.simulation() et appliquer uniquement leur sous-partie respective des noeuds afin de gérer les forces que j'ai mentionnées plus tôt. Voici ce que je pensais faire en pseudo-code 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( .... ) ) 

Mais je pense qu'il n'est pas optimal compte tenu du calcul.

Est-il possible d'appliquer une force sur une sous-partie des noeuds de la simulation sans devoir créer d'autres simulations?

Mise en œuvre de la deuxième solution d'altocumulus:

J'ai essayé cette solution comme ceci:

 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); }); 

Mon réseau de centroides peut changer, c'est pourquoi j'ai dû mettre en œuvre une façon dynamique de mettre en œuvre mes sous-forces. Cependant, je finis par avoir cette erreur à travers les tiques de simulation:

 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 

J'ai conclu que le tableau filtré n'est pas affecté aux noeuds , et je ne peux pas comprendre pourquoi. PS: J'ai vérifié avec console.log: nodes.filter (…) renvoie un tableau rempli, donc ce n'est pas l'origine du problème.

Pour appliquer une force à un seul sous-ensemble de noeuds, vous avez essentiellement des options:

  1. Implémentez votre propre force, ce qui n'est pas aussi difficile que possible, car

    Une force est simplement une fonction qui modifie les positions ou les vitesses des nœuds;

– ou, si vous voulez adhérer aux forces standard –

  1. Créez une force standard et force.initialize() sa méthode force.initialize() , qui

    Affecte le rang de noeuds à cette force.

    En filtrant les nœuds et en n'attribuant que ceux qui vous intéressent, vous pouvez contrôler les nœuds sur lesquels la force devrait agir:

     // 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 } 

L'extrait suivant démontre la deuxième approche en initialisant un d3.forceY avec un sous-ensemble de nœuds. À partir de l'ensemble des cercles distribués de manière aléatoire, seul chaque seconde aura la force appliquée et sera ainsi déplacé vers le bas.

 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 dx; }) .attr("cy", function(d) { return dy; }) } 
 <script src="https://d3js.org/d3.v4.js"></script> 

J'ai réparé ma propre mise en œuvre de la deuxième solution d'altocumulus:

Dans mon cas, j'ai dû créer deux forces par centroïde . Il semble que nous ne pouvons pas partager le même initialisateur pour toutes les fonctions forceX et forceY.

J'ai dû créer des variables locales pour chaque centroïde sur la boucle:

 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); });