Как добиться формы диска в симуляции силы D3? - PullRequest
0 голосов
/ 24 января 2019

Я пытаюсь воссоздать потрясающую визуализацию «точечного потока» из Вышедшего из строя Нади Бремер и Ширели Ву.

original bubbles

Меня особенно заинтриговала очень круглая форма «пузырей» и сжатие, похожее на гидродинамику, в месте, где точки достигают пузыря (черная стрелка).

Я хотел создать (три) фиксированных узла с помощью .fx и .fy (черные точки) и связать все остальные узлы с соответствующим фиксированным узлом. Результат выглядит довольно растрепанным, и пузырьки даже не образуются вокруг их центральных узлов, когда я уменьшаю силы, чтобы анимация работала немного медленнее.

  const simulation = d3.forceSimulation(nodes)
    .force("collide", d3.forceCollide((n, i) => i < 3 ? 0 : 7))
    .force("links", d3.forceLink(links).strength(.06))

Какие-нибудь идеи по настройке силы, которые могли бы дать более эстетически приятные результаты?

Я понимаю, что мне придется со временем анимировать групповое задание, чтобы получить эффект «струйки» (в противном случае все точки просто попали бы к месту назначения), но я бы хотел начать с хорошего и круглого установившееся состояние симуляции.

EDIT

Я проверил исходный код, и он просто воспроизводит предварительно записанные данные моделирования, я полагаю, из соображений производительности.

my result

Ответы [ 3 ]

0 голосов
/ 25 января 2019

Построение от начала Герардо,

Я думаю, что одним из ключевых моментов, чтобы избежать чрезмерной энтропии, является определение затухания скорости - это поможет избежать превышения заданного местоположения.Слишком медленно, вы не получите увеличения плотности, когда поток останавливается слишком быстро, и у вас есть узлы, которые слишком перепутаны или превышают их пункт назначения, колебаясь между слишком большим и слишком коротким.

A многиесила тела здесь полезна - она ​​может удерживать узлы на расстоянии (а не от силы столкновения), причем отталкивание между узлами компенсируется позиционными силами для каждого кластера.Ниже я использовал две точки центрирования и свойство узла, чтобы определить, какая из них используется.Эти силы должны быть довольно слабыми - сильные силы довольно легко приводят к коррекции.

Вместо того, чтобы использовать таймер, я использую функцию Simulation.find () каждый тик, чтобы выбрать один узел из одного кластера иПереключите, какой центр он привлекает.После 1000 тиков симуляция, приведенная ниже, остановится:

var canvas = d3.select("canvas");
var width = +canvas.attr("width");
var height = +canvas.attr("height");
var context = canvas.node().getContext('2d');

// Key variables:
var nodes = [];
var strength = -0.25;         // default repulsion
var centeringStrength = 0.01; // power of centering force for two clusters
var velocityDecay = 0.15;     // velocity decay: higher value, less overshooting
var outerRadius = 250;        // new nodes within this radius
var innerRadius = 100;        // new nodes outside this radius, initial nodes within.
var startCenter = [250,250];  // new nodes/initial nodes center point
var endCenter = [710,250];	  // destination center
var n = 200;		          // number of initial nodes
var cycles = 1000;	          // number of ticks before stopping.



// Create a random node:
var random = function() {
	var angle = Math.random() * Math.PI * 2;
	var distance = Math.random() * (outerRadius - innerRadius) + innerRadius;
	var x = Math.cos(angle) * distance + startCenter[0];
	var y = Math.sin(angle) * distance + startCenter[1];

	return { 
	   x: x,
	   y: y,
	   strength: strength,
	   migrated: false
	   }
}

// Initial nodes:
for(var i = 0; i < n; i++) {
	nodes.push(random());
}
	
var simulation = d3.forceSimulation()
    .force("charge", d3.forceManyBody().strength(function(d) { return d.strength; } ))
	.force("x1",d3.forceX().x(function(d) { return d.migrated ? endCenter[0] : startCenter[0] }).strength(centeringStrength))
	.force("y1",d3.forceY().y(function(d) { return d.migrated ? endCenter[1] : startCenter[1] }).strength(centeringStrength))
	.alphaDecay(0)
	.velocityDecay(velocityDecay)
    .nodes(nodes)
    .on("tick", ticked);

var tick = 0;
	
function ticked() {
	tick++;
	
	if(tick > cycles) this.stop();
	
	nodes.push(random()); // create a node
	this.nodes(nodes);    // update the nodes.

  var migrating = this.find((Math.random() - 0.5) * 50 + startCenter[0], (Math.random() - 0.5) * 50 + startCenter[1], 10);
  if(migrating) migrating.migrated = true;
  
	
	context.clearRect(0,0,width,height);
	
	nodes.forEach(function(d) {
		context.beginPath();
		context.fillStyle = d.migrated ? "steelblue" : "orange";
		context.arc(d.x,d.y,3,0,Math.PI*2);
		context.fill();
	})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<canvas width="960" height="500"></canvas>

Вот вид блока (фрагмент будет лучше полной страницы, параметры предназначены для него).Начальные узлы формируются в том же кольце, что и более поздние узлы (поэтому на начальном этапе есть некоторая путаница, но это легко исправить).На каждом тике создается один узел и делается одна попытка перенести узел рядом с серединой на другую сторону - таким образом создается поток (в отличие от любого случайного узла).

Для жидкостейлучше всего использовать несвязанные узлы (я использовал их для моделирования ветра) - связанные узлы идеальны для структурированных материалов, таких как сетки или ткани.И, как и Герардо, я также фанат работ Надии, но в будущем мне придется следить за работой Ширли.

0 голосов
/ 25 января 2019

С помощью других ответов здесь я продолжал экспериментировать, и я хотел бы обобщить мои выводы:

Форма диска

forceManyBody, кажется,быть более стабильным, чем forceCollide.Ключ для его использования без искажения формы диска - .distanceMax.С другой стороны, ваша визуализация больше не является «безмасштабной» и должна быть настроена вручную.В качестве руководства, превышение в каждом направлении вызывает различные артефакты:

Установка distanceMax слишком высокая деформирует соседние диски.

distanceMax too high

Параметр distanceMax слишком низкий (меньше ожидаемого диаметра диска):

enter image description here

Этот артефакт можно увидеть в визуализации Guardian (когда красный и синийточки в конечном итоге образуют огромный диск), поэтому я вполне уверен, что использовалось distanceMax.

позиционирование узла

Я все еще нахожу использование forceX сforceY и пользовательские функции доступа слишком громоздки для более сложной анимации.Я решил пойти с «контрольными» узлами, и с небольшой настройкой (chargeForce.strength(-4), link.strength(.2).distance(1)) все работает нормально.

Жидкое ощущение

Во время экспериментов сНастройки, которые я заметил, что ощущение текучести (входящие узлы раздвигают границу принимающего диска) особенно зависит от simulation.velocityDecay, но слишком сильное его понижение добавляет слишком большую энтропию системе.*

Мой пример кода разбивает одну «популяцию» на три, а затем на пять - отметьте это на блоков .Каждый из приемников представлен управляющим узлом.Узлы переназначаются новым приемникам партиями, что дает больший контроль над визуальным отображением «потока».Начало выбора узлов для назначения ближе к приемникам выглядит более естественным (один sort в начале каждой анимации).

0 голосов
/ 24 января 2019

Надя Бремер - мой кумир в визуализации D3, она абсолютная звезда! (исправление после комментария ОП : кажется, что данные были созданы Ширли Ву ... в любом случае, это не меняет того, что я сказал о Бремере).

Первая попытка выяснить, что происходит на этой странице, - взглянуть на исходный код , который, к сожалению, является геркулесовой работой. Итак, оставшийся вариант - попытаться воспроизвести это.

Задача здесь не в том, чтобы создать круговой узор, это довольно просто: вам нужно только объединить forceX, forceY и forceCollide:

const svg = d3.select("svg")
const data = d3.range(500).map(() => ({}));

const simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(200))
  .force("y", d3.forceY(120))
  .force("collide", d3.forceCollide(4))
  .stop();

for (let i = 300; i--;) simulation.tick();

const circles = svg.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("r", 2)
  .style("fill", "tomato")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="300"></svg>

Настоящая проблема здесь - это перемещение этих кругов в заданный симулятор один за другим , но не все одновременно, как я сделал здесь .

Итак, это мое предложение / попытка:

Мы создаем симуляцию, которую мы останавливаем ...

simulation.stop();

Затем по таймеру ...

const timer = d3.interval(function() {etc...

... мы добавляем узлы к симуляции:

const newData = data.slice(0, index++)
simulation.nodes(newData);

Это результат, нажмите кнопку:

const radius = 2;
let index = 0;
const limit = 500;
const svg = d3.select("svg")
const data = d3.range(500).map(() => ({
  x: 80 + Math.random() * 40,
  y: 80 + Math.random() * 40
}));

let circles = svg.selectAll(null)
  .data(data);
circles = circles.enter()
  .append("circle")
  .attr("r", radius)
  .style("fill", "tomato")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y)
  .style("opacity", 0)
  .merge(circles);

const simulation = d3.forceSimulation()
  .force("x", d3.forceX(500))
  .force("y", d3.forceY(100))
  .force("collide", d3.forceCollide(radius * 2))
  .stop();

function ticked() {
  circles.attr("cx", d => d.x)
    .attr("cy", d => d.y);
}

d3.select("button").on("click", function() {
  simulation.on("tick", ticked).restart();
  const timer = d3.interval(function() {
    if (index > limit) timer.stop();
    circles.filter((_, i) => i === index).style("opacity", 1)
    const newData = data.slice(0, index++)
    simulation.alpha(0.25).nodes(newData);
  }, 5)
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Click</button>
<svg width="600" height="200"></svg>

Проблемы с этим подходом

Как видите, здесь слишком много энтропии, особенно в центре. Надя Бремер / Ширли Ву, вероятно, использовали более изощренный код. Но это мои два цента на данный момент, давайте посмотрим, появятся ли другие ответы с другими подходами.

...