Почему узлы с принудительной компоновкой переходят из исходного положения при обновлении - PullRequest
1 голос
/ 29 марта 2020

Почему мои круги прыгают с (0,0), когда я обновляю свои данные каждую итерацию?

Я хочу сделать раскладку с кругами, которые изменяют радиус при обновлении данных. Я не могу понять, как использовать силы d3 внутри al oop. Все, что я могу получить, это круги, которые прыгают от начала координат, пока они меняют свои размеры. Я полагаю, что проблема заключается в том, как d3 хранит и устанавливает координаты объектов.

Вот мой код:

var tickDuration = 1000;
var margin = {top: 80, right: 60, bottom: 60, left: 60}
const width = 960 - margin.left - margin.right,
    height = 600 - margin.top - margin.bottom;
let step = 0;

data = [
    {
        "name": "A",
        "value": 99,
        "step": 9
    },
    {
        "name": "A",
        "value": 28,
        "step": 8
    },
    {
        "name": "A",
        "value": 27,
        "step": 7
    },
    {
        "name": "A",
        "value": 26,
        "step": 6
    },
    {
        "name": "A",
        "value": 25,
        "step": 5
    },
    {
        "name": "A",
        "value": 24,
        "step": 4
    },
    {
        "name": "A",
        "value": 23,
        "step": 3
    },
    {
        "name": "A",
        "value": 22,
        "step": 2
    },
    {
        "name": "A",
        "value": 21,
        "step": 1
    },
    {
        "name": "A",
        "value": 20,
        "step": 0
    },
    {
        "name": "B",
        "value": 19,
        "step": 9
    },
    {
        "name": "B",
        "value": 18,
        "step": 8
    },
    {
        "name": "B",
        "value": 17,
        "step": 7
    },
    {
        "name": "B",
        "value": 16,
        "step": 6
    },
    {
        "name": "B",
        "value": 150,
        "step": 5
    },
    {
        "name": "B",
        "value": 14,
        "step": 4
    },
    {
        "name": "B",
        "value": 13,
        "step": 3
    },
    {
        "name": "B",
        "value": 12,
        "step": 2
    },
    {
        "name": "B",
        "value": 11,
        "step": 1
    },
    {
        "name": "B",
        "value": 10,
        "step": 0
    },
    {
        "name": "С",
        "value": 39,
        "step": 9
    },
    {
        "name": "С",
        "value": 38,
        "step": 8
    },
    {
        "name": "С",
        "value": 37,
        "step": 7
    },
    {
        "name": "С",
        "value": 36,
        "step": 6
    },
    {
        "name": "С",
        "value": 35,
        "step": 5
    },
    {
        "name": "С",
        "value": 34,
        "step": 4
    },
    {
        "name": "С",
        "value": 33,
        "step": 3
    },
    {
        "name": "С",
        "value": 32,
        "step": 2
    },
    {
        "name": "С",
        "value": 31,
        "step": 1
    },
    {
        "name": "С",
        "value": 30,
        "step": 0
    }
];


const halo = function (text, strokeWidth) {
    text.select(function () {
        return this.parentNode.insertBefore(this.cloneNode(true), this);
    })
        .style('fill', '#ffffff')
        .style('stroke', '#ffffff')
        .style('stroke-width', strokeWidth)
        .style('stroke-linejoin', 'round')
        .style('opacity', 1);

}

var svg = d3.select('body').append('svg')
    .attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom)
    .append('g')
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')

let rad = d3.scaleSqrt()
    .domain([0, 100])
    .range([0, 200]);

var fCollide = d3.forceCollide().radius(function (d) {
    return rad(d.value) + 2
});
    fcharge = d3.forceManyBody().strength(0.05)
    fcenter = d3.forceCenter(width / 2, height / 2)

var Startsimulation = d3.forceSimulation()
    .force('charge', fcharge)
    //.force('center', fcenter)
    // .force("forceX", d3.forceX(width/2).strength(.2))
    // .force("forceY", d3.forceY(height/2).strength(.2))
    .force("collide", fCollide)


function ticked() {
    d3.selectAll('.circ')
        .attr('r', d => rad(d.value))
        .attr("cx", function (d) {
            return d.x = Math.max(rad(d.value), Math.min(width - rad(d.value), d.x));
        })

        .attr("cy", function (d) {
            return d.y = Math.max(rad(d.value), Math.min(height - rad(d.value), d.y));
        })

    d3.selectAll('.label')
        .attr("cx", function (d) {
            return d.x = Math.max(rad(d.value), Math.min(width - rad(d.value), d.x));
        })
        .attr("cy", function (d) {
            return d.y = Math.max(rad(d.value), Math.min(height - rad(d.value), d.y));
        });

}


data.forEach(d => {
    d.value = +d.value
    d.value = isNaN(d.value) ? 0 : d.value,
        d.step = +d.step,
        d.colour = d3.hsl(Math.random() * 360, 0.6, 0.6)
});

let stepSlice = data.filter(d => d.step == step && !isNaN(d.value))
    .sort((a, b) => b.value - a.value)


let stepText = svg.append('text')
    .attr('class', 'stepText')
    .attr('x', width - margin.right)
    .attr('y', height - 25)
    .style('text-anchor', 'end')
    .html(~~step)
    .call(halo, 10);

svg.selectAll('circle.circ')
    .data(stepSlice, d => d.name)
    .enter()
    .append('circle')
    .attr('class', 'circ')
    .attr('r', d => rad(d.value))
    .style('fill', d => d.colour)
    .style("fill-opacity", 0.8)
    .attr("stroke", "black")
    .style("stroke-width", 1)

Startsimulation.nodes(stepSlice).on('tick', ticked)


let ticker = d3.interval(e => {

    stepSlice = data.filter(d => d.step == step && !isNaN(d.value))
        .sort((a, b) => b.value - a.value)

    // rad.domain([0, d3.max(stepSlice, d => d.value)]);
    let circles = d3.selectAll('.circ').data(stepSlice, d => d.name)

    circles

        .transition()
        .duration(tickDuration)
        .ease(d3.easeLinear)
        .attr('r', d => rad(d.value))


    Startsimulation
        .nodes(stepSlice)
        .alpha(1)
        .alphaTarget(0.3)


    stepText.html(~~step);
    if (step == 9) ticker.stop();
    step = d3.format('.1f')((+step) + 1);
}, tickDuration);
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <style>
        text.stepText{
            font-size: 64px;
            font-weight: 700;
            opacity: 0.25;}
    </style>
</head>
<body>
<div id="chart"></div>

<script src="https://d3js.org/d3.v5.js"></script>
</body>
</html>

1 Ответ

2 голосов
/ 30 марта 2020

Основная проблема связана с d3-force, учитывая ваш код, но я собираюсь предложить альтернативный метод создания этой визуализации (если я правильно понимаю). Эта предложенная альтернатива будет более соответствовать шаблонам, для которых была разработана D3.

Предлагаемое решение решит проблему с помощью d3-force, но также упростит привязку данных. Я попытаюсь объяснить, почему другой подход может быть предпочтительным в обеих точках ниже:

D3 и привязка данных

D3 уделяет значительное внимание привязке данных. Элементы в массиве данных связаны с элементами в DOM.

Вообще говоря, лучшим источником данных для использования в D3 является тот, в котором данные являются массивом, а каждый элемент в этом массиве данных представлен элементом в DOM. Таким образом, мы можем легко вводить, обновлять, выходить.

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

Более простой метод с D3 - структурировать ваши данные по-другому. У вас есть три круга, поэтому в вашем массиве данных должно быть три объекта. Эти объекты должны иметь свойства для хранения данных шага. Затем мы можем обновить круги в зависимости от того, на каком этапе мы находимся. Нет повторного выбора, повторного связывания, фильтрации и т. Д. c. Недостатком является то, что это часто требует реструктуризации ваших данных до того, как вы попадете в код визуализации, но отдача того стоит, когда вы начнете делать визуализацию.

D3 Force Simulation

Метод узлов моделирования силы D3 принимает массив объектов. Если эти объекты не имеют свойств x, y, vx, vy, имитация силы изменяет эти объекты, чтобы дать им эти свойства (для этого они не клонируют эти объекты). Это инициализация узлов. Если вы замените исходные узлы новыми, симуляция инициализирует новые узлы. Нет ссылки на предыдущие узлы - узлы сами являются объектами (вы не заменяете часть данных узла, вы заменяете весь узел).

Для решения этой проблемы в текущем подходе нам нужно было бы взять свойства x, y, vx, vy каждого узла текущего шага и назначить эти свойства узлам следующего шага в начале каждого шага.

Вместо этого давайте теперь симуляция силы будет сохранять одни и те же узлы все время. Как и выше, у нас есть три узла, поэтому у нас есть массив данных из трех объектов, в каждом из которых содержатся все данные шага. Теперь нам не нужно фильтровать узлы, передавать свойства и т. Д. c.

Альтернативный подход

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

let data = [
  {
    "name": "A",
    "steps": [20,21,22,23,24,25,26,27,28,99],
    "colour": "steelblue"   
  },
  {
    "name": "B",
    "steps": [10,11,12,13,14,150,16,17,18,19],
    "colour":"crimson"
  },
  {
    "name": "С",
    "steps": [30,31,32,33,34,35,36,37,38,39],
    "colour":"orange"
  }
];

Теперь, когда мы хотим перейти на n-й шаг, мы можем получить текущий радиус с помощью someScale(d.steps[n]). Это позволяет нам устанавливать нарисованный радиус и радиус столкновения без привязки данных к элементам DOM и без предоставления новых узлов силе.

Теперь мы можем настроить все, что не зависит от шага:

  • SVG
  • Весы
  • Функция тика
  • Данные
  • Большинство настроек симуляции
  • Введите элементы SVG

Однако вам не нужно устанавливать начальный радиус на значение первого шага, когда вы добавьте круги, потому что мы будем делать это каждый раз, когда мы перемещаем шаг. Нам просто нужны свойства stati c каждого элемента SVG (тем не менее, я установил начальные радиусы ниже нуля, чтобы мы могли плавно переходить).

Затем мы можем поместить весь код, который изменяется с каждым шагом в интервальной функции. Этот код будет делать только несколько вещей:

  • Увеличение до следующего шага
  • Установите новую силу столкновения с новыми радиусами
  • Переходите радиусы окружностей
  • Примените новую силу столкновения и разогрейте симуляцию.
  • Обновите текст, который показывает на каком этапе мы находимся.

Примечания

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

Стоит отметить одну вещь о переходах и раскладках. Если вы измените радиус и установите радиус в функции тика, функция тика и переход будут постоянно перекрывать друг друга. Переходы должны изменять атрибуты, которые не были изменены в тике, и наоборот. В вашем коде вы изменяете каждый кружок r и в галочке, и в переходе.

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

У меня есть функция ниже (radius), которая использует текущий шаг и данные для возврата радиуса. Поскольку я хотел использовать именно это, где мне нужен радиус круга, я начинаю с шага -1. Причина в том, что мне нужно увеличить step в начале функции nextStep. Если я увеличиваю значение в конце этой функции, то функция ticked, которая работает непрерывно, будет использовать шаг, отличный от остальной функции nextStep. Функция radius была написана так, что если шаг равен -1, он будет использовать нулевой шаг, чтобы избежать проблем при инициализации. Могут быть более хорошие способы справиться с этим, я чувствовал, что это было самым простым.

Я могу добавить больше комментариев, если необходимо, но я надеюсь, что приведенного выше объяснения и моих ограниченных комментариев достаточно:

const tickDuration = 1000,
      margin = {top: 80, right: 60, bottom: 60, left: 60},
      width = 960 - margin.left - margin.right,
      height = 600 - margin.top - margin.bottom;

let data = [
  {
    "name": "A",
    "steps": [20,21,22,23,24,25,26,27,28,99],
    "colour": "steelblue"   
  },
  {
    "name": "B",
    "steps": [10,11,12,13,14,150,16,17,18,19],
    "colour":"crimson"
  },
  {
    "name": "С",
    "steps": [30,31,32,33,34,35,36,37,38,39],
    "colour":"orange"
  }
];

let step = -1;
let svg = d3.select('body').append('svg')
    .attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom)
    .append('g')
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')

// No need to set the text value yet: we'll do that in the interval.
let stepText = svg.append('text')
    .attr('x',10) // repositioned for snippet view.
    .attr('y', 10)

// Scale as before.
let scale = d3.scaleSqrt()
    .domain([0, 100])
    .range([0,  100]);

// Get the right value for the scale:
let radius = function(d) {
  return scale(d.steps[Math.max(step,0)]);
}

// Only initial or static properties - no data driven properties:
let circles = svg.selectAll('circle.circ')
    .data(data, d => d.name)
    .enter()
    .append('circle')
    .attr("r", 0) // transition from zero.
    .style('fill', d => d.colour)

// Set up forcesimulation basics:
let simulation = d3.forceSimulation()
    .force('charge', d3.forceManyBody().strength(100))
    .nodes(data)
    .on('tick', ticked)
    
// Set up the ticked function for the force simulation:
function ticked() {
  circles
    .attr("cx", function (d) {
      return d.x = Math.max(radius(d), Math.min(width - radius(d), d.x));
    })
    .attr("cy", function (d) {
      return d.y = Math.max(radius(d), Math.min(height - radius(d), d.y));
    })
}

// Advance through the steps:
let ticker = d3.interval(nextStep, tickDuration);

function nextStep() {
  step++;
   
  // Update circles
  circles.transition()
    .duration(tickDuration*0.5)
    .ease(d3.easeLinear)
    .attr('r', radius)

  // Set collision force:
  var collide = d3.forceCollide()
    .radius(function (d) {  return radius(d) + 2 })
  
  // Update force
  simulation
    .force("collide", collide)
    .alpha(1)
    .restart();

  // Update text
  stepText.text(step);
  // Check to see if we stop.
  if (step == 9) ticker.stop();
};
nextStep(); // Start first step without delay.
text { 
  font-size: 64px;
  font-weight: 700;
  opacity: 0.25;
}

circle {
  fill-opacity: 0.8;
  stroke: black;
  stroke-width: 1px;
}
<div id="chart"></div>
<script src="https://d3js.org/d3.v5.js"></script>
...