Как анимировать переход по сгруппированным вложенным координатным данным json с помощью D3.js?

Я новичок в D3 и JS и пытаюсь понять, где я ошибаюсь. У меня есть данные временного ряда x/y, которые я хочу анимировать с помощью D3. Целью является график, который показывает все 4 точки и перемещает их через каждую из 5 меток времени в выборке данных со скоростью, которая рассчитывается с меткой времени.

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

[{"time": 0, "info":[{"id": "A", "x": 10, "y": 20},
                    {"id": "B", "x": 40, "y": 90},
                    {"id": "C", "x": 10, "y": 90},
                    {"id": "D", "x": 80, "y": 70}]},
 {"time": 0.5, "info":[{"id": "A", "x": 20, "y": 30},
                       {"id": "B", "x": 60, "y": 70},
                       {"id": "C", "x": 100, "y": 10},
                       {"id": "D", "x": 10, "y": 32}]},
 {"time": 1, "info":[{"id": "A", "x": 50, "y": 60},
                     {"id": "B", "x": 0, "y": 0},
                     {"id": "C", "x": 80, "y": 10},
                     {"id": "D", "x": 50, "y": 50}]},
 {"time": 1.5, "info":[{"id": "A", "x": 40, "y": 50},
                       {"id": "B", "x": 100, "y": 30},
                       {"id": "C", "x": 80, "y": 90},
                       {"id": "D", "x": 80, "y": 40}]},
 {"time": 2, "info":[{"id": "A", "x": 60, "y": 10},
                     {"id": "B", "x": 0, "y": 50},
                     {"id": "C", "x": 10, "y": 50},
                     {"id": "D", "x": 30, "y": 60}]}]

Я могу построить отдельные моменты времени для этих данных, но есть несколько проблем при попытке анимировать это: Вот Fiddle с моим кодом. Продолжительность перехода задается разницей между текущим временем и предыдущим временем.

Кажется, что существует много способов объединения переходов, повторяющих данные, но они кажутся неэффективными. Вот как я заставил его (частично) работать, это также можно увидеть в Fiddle.

var data = [{"time": 0, "info":[{"id": "A", "x": 10, "y": 20},
                                                        {"id": "B", "x": 40, "y": 90},
                                    {"id": "C", "x": 10, "y": 90},
                                    {"id": "D", "x": 80, "y": 70}]},
        {"time": 0.5, "info":[{"id": "A", "x": 20, "y": 30},
                                                        {"id": "B", "x": 60, "y": 70},
                            {"id": "C", "x": 100, "y": 10},
                            {"id": "D", "x": 10, "y": 32}]},
        {"time": 1, "info":[{"id": "A", "x": 50, "y": 60},
                           {"id": "B", "x": 80, "y": 50},
                           {"id": "C", "x": 80, "y": 10},
                           {"id": "D", "x": 50, "y": 50}]},
                {"time": 1.5, "info":[{"id": "A", "x": 40, "y": 50},
                           {"id": "B", "x": 100, "y": 30},
                           {"id": "C", "x": 80, "y": 90},
                           {"id": "D", "x": 80, "y": 40}]},
        {"time": 2, "info":[{"id": "A", "x": 60, "y": 10},
                              {"id": "B", "x": 0, "y": 50},
                              {"id": "C", "x": 10, "y": 50},
                              {"id": "D", "x": 30, "y": 60}]}]
                           

 var margin = {top: 30, right: 30, bottom: 30, left: 30},
        width = 300 - margin.left - margin.right,
        height = 300 - margin.top - margin.bottom;

    // append the svg object to the body of the page
    var svg = d3.select("#container")
      .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 + ")");

     // Add X axis
    var x = d3.scaleLinear()
      .domain([0, 100])
      .range([ 0, width ]);
    svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));

    // Add Y axis
    var y = d3.scaleLinear()
      .domain([0, 100])
      .range([ height, 0]);
    svg.append("g")
      .call(d3.axisLeft(y));
  function runTheSimulation() {
        var id = 0
        if (id == 0) {
      var points = svg.selectAll('circle')
          .data(data[0].info, function(d, i) {return d.id})

          points.enter().append('circle')
            .attr('cx', function(d) {return x(d.x)})
            .attr('cy', function(d) {return y(d.y)})
            .attr('r', 8)
            .attr('fill', 'white')
            .attr('stroke', 'black')
            
            id++;
            next_frame()

      } else {
      next_frame()
      }
      function next_frame() {
      
        var delta_time = (data[id].time - data[id-1].time)*1000
        var players = svg.selectAll('circle')
          .data(data[id].info, function(d) {return d.id})
          .transition()
          .duration(delta_time)
          .ease(d3.easeLinear)
          .attr('cx', function(d) {return x(d.x)})
          .attr('cy', function(d) {return y(d.y)})
          .on('end', function() {
                id++;
              //console.info(id)
              if (id >= data.length) {
                return;
              }
              next_frame();
          })
        }
    }
        
body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
}

#app {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  transition: all 0.2s;
}

li {
  margin: 8px 0;
}

h2 {
  font-weight: bold;
  margin-bottom: 15px;
}

.done {
  color: rgba(0, 0, 0, 0.3);
  text-decoration: line-through;
}

input {
  margin-right: 5px;
}
  <!-- Bootstrap CSS -->
  <link rel = "stylesheet" href = "http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <!-- jQuery -->
  <script src = "https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Bootstrap JavaScript -->
  <script src = "http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
  <!-- Load d3.js -->
  <script src = "https://d3js.org/d3.v4.js"></script>
</head>
<body>
<button onclick = "runTheSimulation()">RUN THE SIMULATION</button>
<div id = "container"></div>

Проблемы, которые у меня есть:

  1. Некоторые элементы пропускаются при переходе. Первый запуск показывает только три раза, а все последующие запуски показывают только два раза. Например: B никогда не переходит в (0,0)
  2. Я console.info() индекс (id) при повторении json и каким-то образом удваиваю его длину?

Спасибо за любые мысли и помощь!

Мне трудно понять, как вы хотите, чтобы это произошло. Например, хотите ли вы, чтобы точка А начиналась с "x": 10, "y": 20, затем двигалась к "x": 50, "y": 60, затем к "x": 40, "y": 50 и затем к "x": 60, "y": 10; с переходом 0,5 с между каждым?

Mark 18.12.2020 16:35

Да, это именно так. Четыре точки одновременно перемещаются к своим соответствующим координатам для каждой временной метки в данных.

Adam_K 18.12.2020 17:15
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
4
2
272
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

<head>
  <!-- Load d3.js -->
  <script src = "https://d3js.org/d3.v4.js"></script>
  <style>

  </style>
</head>
<body>
  <button onclick = "runTheSimulation()">RUN THE SIMULATION</button>
  <div id = "container"></div>
  <script>
    var data = [
      {
        time: 0,
        info: [
          { id: 'A', x: 10, y: 20 },
          { id: 'B', x: 40, y: 90 },
          { id: 'C', x: 10, y: 90 },
          { id: 'D', x: 80, y: 70 },
        ],
      },
      {
        time: 0.5,
        info: [
          { id: 'A', x: 20, y: 30 },
          { id: 'B', x: 60, y: 70 },
          { id: 'C', x: 100, y: 10 },
          { id: 'D', x: 10, y: 32 },
        ],
      },
      {
        time: 1,
        info: [
          { id: 'A', x: 50, y: 60 },
          { id: 'B', x: 80, y: 50 },
          { id: 'C', x: 80, y: 10 },
          { id: 'D', x: 50, y: 50 },
        ],
      },
      {
        time: 1.5,
        info: [
          { id: 'A', x: 40, y: 50 },
          { id: 'B', x: 100, y: 30 },
          { id: 'C', x: 80, y: 90 },
          { id: 'D', x: 80, y: 40 },
        ],
      },
      {
        time: 2,
        info: [
          { id: 'A', x: 60, y: 10 },
          { id: 'B', x: 0, y: 50 },
          { id: 'C', x: 10, y: 50 },
          { id: 'D', x: 30, y: 60 },
        ],
      },
    ];

    var margin = { top: 30, right: 30, bottom: 30, left: 30 },
      width = 300 - margin.left - margin.right,
      height = 300 - margin.top - margin.bottom;

    // append the svg object to the body of the page
    var svg = d3
      .select('#container')
      .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 + ')');

    // Add X axis
    var x = d3.scaleLinear().domain([0, 100]).range([0, width]);
    svg
      .append('g')
      .attr('transform', 'translate(0,' + height + ')')
      .call(d3.axisBottom(x));

    // Add Y axis
    var y = d3.scaleLinear().domain([0, 100]).range([height, 0]);
    svg.append('g').call(d3.axisLeft(y));
    
    
    function runTheSimulation() {

        var chainedTransition,
            prevTime = 0,
            elapsedTime = 0;

        // create a null selection which reprensents each timepiont
        svg
          .selectAll(null)
          .data(data)
          .enter()
          .each(function(d,i){

            // for each timepoint, update the data
            points = svg.selectAll('circle')
              .data(d.info, (d) => d.id);

            // handle enter to create circles first time through
            points
              .enter()
              .append('circle')
              .attr('r', 8)
              .attr('fill', 'white')
              .attr('stroke', 'black')
              .attr('cx', d => x(d.x))
              .attr('cy', d => y(d.y));   

            // calculate how long we'll transition 
            // between this timepoint and last
            let transTime = (d.time - prevTime) * 1000;

            // chain the transition for all data updates
            chainedTransition = points
              .transition()
              .duration(transTime)
              .delay(elapsedTime)
              .ease(d3.easeLinear)
              .attr('cx', d => x(d.x))
              .attr('cy', d => y(d.y));

            // rememeber this time
            prevTime = d.time;
            // and elpased time for chaining
            elapsedTime += transTime;

          });
    }
  </script>
</body>

Это фантастика, спасибо! Формат очень чистый и простой для понимания. Я собираюсь изучить каждую функцию, я думаю, что это большой пробел в моих знаниях D3.

Adam_K 18.12.2020 18:45

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

Andrew Reid 18.12.2020 18:46

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

Mark 18.12.2020 19:21

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

var data = [ {"id":"A", frames: [{time:0, x: 1},{time:1,x: 2}]}, ...

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

Я использовал следующую структуру:

Который я получил, запустив ваш код через базовое транспонирование, расположенное во фрагменте ниже.

Каждый элемент в массиве данных представляет один элемент в DOM — в соответствии с идиомой D3. Индекс текущего отрисовываемого кадра хранится в d.currentFrame, а все кадры хранятся в d.frames.

В основном ваша функциональность перехода будет довольно простой:

  function transition(d) {
  
    // Is there another frame?
    if (d.currentFrame == d.frames.length-1 ) return; // don't keep going if there is no more data.
    
    // Change in time
    var dt = -d.frames[d.currentFrame++].time + d.frames[d.currentFrame].time;

    // Do the transition:
    d3.select(this)
     .transition()
     .duration(dt*1000)
     .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
     .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})   
     .on("end", transition) // and repeat for this element
   
  } 

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

  function runTheSimulation() {
    svg.selectAll("circle")
     .each(function(d) { d.currentFrame = 0; })
     .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
     .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
     .each(transition);
  }

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

Собираем вместе:

var data = [{"time": 0, "info":[{"id": "A", "x": 10, "y": 20},
                                                        {"id": "B", "x": 40, "y": 90},
                                    {"id": "C", "x": 10, "y": 90},
                                    {"id": "D", "x": 80, "y": 70}]},
        {"time": 0.5, "info":[{"id": "A", "x": 20, "y": 30},
                                                        {"id": "B", "x": 60, "y": 70},
                            {"id": "C", "x": 100, "y": 10},
                            {"id": "D", "x": 10, "y": 32}]},
        {"time": 1, "info":[{"id": "A", "x": 50, "y": 60},
                           {"id": "B", "x": 80, "y": 50},
                           {"id": "C", "x": 80, "y": 10},
                           {"id": "D", "x": 50, "y": 50}]},
                {"time": 1.5, "info":[{"id": "A", "x": 40, "y": 50},
                           {"id": "B", "x": 100, "y": 30},
                           {"id": "C", "x": 80, "y": 90},
                           {"id": "D", "x": 80, "y": 40}]},
        {"time": 2, "info":[{"id": "A", "x": 60, "y": 10},
                              {"id": "B", "x": 0, "y": 50},
                              {"id": "C", "x": 10, "y": 50},
                              {"id": "D", "x": 30, "y": 60}]}];


// Manipulate array:
var newData = data[0].info.map((_, i) => { return  {currentFrame: 0, id: data[0].info[i].id ,frames:data.map((row,t) => { return { x: row.info[i].x, y: row.info[i].y, time: data[t].time }}) } });

var margin = {top: 30, right: 30, bottom: 30, left: 30},
        width = 300 - margin.left - margin.right,
        height = 300 - margin.top - margin.bottom;

    // append the svg object to the body of the page
    var svg = d3.select("#container")
      .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 + ")");

     // Add X axis
    var x = d3.scaleLinear()
      .domain([0, 100])
      .range([ 0, width ]);
    svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));

    // Add Y axis
    var y = d3.scaleLinear()
      .domain([0, 100])
      .range([ height, 0]);
    svg.append("g")
      .call(d3.axisLeft(y));
      
      
   var points = svg.selectAll('circle')
          .data(newData)
          .enter()
          .append('circle')
            .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
            .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
            .attr('r', 8)
            .attr('fill', 'white')
            .attr('stroke', 'black')

  
  function runTheSimulation() {
    svg.selectAll("circle")
     .each(function(d) { d.currentFrame = 0; })
     .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
     .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
     .each(transition);
  }
  
  function transition(d) {
  
    // Is there another frame?
    if (d.currentFrame == d.frames.length-1 ) return; // don't keep going if there is no more data.
    
    // Change in time
    var dt = -d.frames[d.currentFrame++].time + d.frames[d.currentFrame].time;

    // Do the transition:
    d3.select(this)
     .transition()
     .duration(dt*1000)
     .attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
     .attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})   
     .on("end", transition)
     
 
  
  } 
body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
}

#app {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  transition: all 0.2s;
}

li {
  margin: 8px 0;
}

h2 {
  font-weight: bold;
  margin-bottom: 15px;
}

.done {
  color: rgba(0, 0, 0, 0.3);
  text-decoration: line-through;
}

input {
  margin-right: 5px;
}
<head>
  <!-- Bootstrap CSS -->
  <link rel = "stylesheet" href = "http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <!-- jQuery -->
  <script src = "https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Bootstrap JavaScript -->
  <script src = "http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
  <!-- Load d3.js -->
  <script src = "https://d3js.org/d3.v4.js"></script>
</head>
<body>
<button onclick = "runTheSimulation()">RUN THE SIMULATION</button>
<div id = "container"></div>
</body>

Другие вопросы по теме