Набор данных изменения и перехода в диаграмме аккордов с D3

Я работаю над диаграммой аккордов, используя D3.

Я пытаюсь сделать так, чтобы, когда пользователь нажимает на ссылку, набор данных изменится на другой предопределенный набор данных. Я просмотрел http://exposedata.com/tutorial/chord/latest.html и http://fleetinbeing.net/d3e/chord.html и попытался использовать некоторые элементы, чтобы заставить его работать,

Вот JavaScript для создания диаграммы "по умолчанию":

var dataset = "data/all_trips.json";

var width = 650,
    height = 600,
    outerRadius = Math.min(width, height) / 2 - 25,
    innerRadius = outerRadius - 18;

var formatPercent = d3.format("%");

var arc = d3.svg.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);

var layout = d3.layout.chord()
    .padding(.03)
    .sortSubgroups(d3.descending)
    .sortChords(d3.ascending);

var path = d3.svg.chord()
    .radius(innerRadius);

var svg = d3.select("#chart_placeholder").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append("g")
    .attr("id", "circle")
    .attr("transform", "translate(" + width / 1.5 + "," + height / 1.75 + ")");

svg.append("circle")
    .attr("r", outerRadius);

d3.csv("data/neighborhoods.csv", function(neighborhoods) {
  d3.json(dataset, function(matrix) {

    // Compute chord layout.
    layout.matrix(matrix);

    // Add a group per neighborhood.
    var group = svg.selectAll(".group")
        .data(layout.groups)
      .enter().append("g")
        .attr("class", "group")
        .on("mouseover", mouseover);

    // Add a mouseover title.
    group.append("title").text(function(d, i) {
      return numberWithCommas(d.value) + " trips started in " + neighborhoods[i].name;
    });

    // Add the group arc.
    var groupPath = group.append("path")
        .attr("id", function(d, i) { return "group" + i; })
        .attr("d", arc)
        .style("fill", function(d, i) { return neighborhoods[i].color; });

    var rootGroup = d3.layout.chord().groups()[0];

    // Text label radiating outward from the group.
    var groupText = group.append("text");

   group.append("svg:text")
        .each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; })
        .attr("xlink:href", function(d, i) { return "#group" + i; })
        .attr("dy", ".35em")
        .attr("color", "#fff")
        .attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
        .attr("transform", function(d) {
          return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" +
            " translate(" + (innerRadius + 26) + ")" +
            (d.angle > Math.PI ? "rotate(180)" : "");
        })
        .text(function(d, i) { return neighborhoods[i].name; });

    // Add the chords.
    var chord = svg.selectAll(".chord")
        .data(layout.chords)
      .enter().append("path")
        .attr("class", "chord")
        .style("fill", function(d) { return neighborhoods[d.source.index].color; })
        .attr("d", path);

    // Add mouseover for each chord.
    chord.append("title").text(function(d) {
      if (!(neighborhoods[d.target.index].name === neighborhoods[d.source.index].name)) {
      return numberWithCommas(d.source.value) + " trips from " + neighborhoods[d.source.index].name + " to " + neighborhoods[d.target.index].name + "\n" +
        numberWithCommas(d.target.value) + " trips from " + neighborhoods[d.target.index].name + " to " + neighborhoods[d.source.index].name;
      } else {
        return numberWithCommas(d.source.value) + " trips started and ended in " + neighborhoods[d.source.index].name;
      }
    });

    function mouseover(d, i) {
      chord.classed("fade", function(p) {
        return p.source.index != i
            && p.target.index != i;
      });
      var selectedOrigin = d.value;
      var selectedOriginName = neighborhoods[i].name;
    }
  });
});

И вот что я пытаюсь сделать, чтобы заново визуализировать диаграмму с новыми данными (есть элемент изображения с id "женский пол".

d3.select("#female").on("click", function () {
  var new_data = "data/women_trips.json";
  reRender(new_data);
});

function reRender(data) {
  var layout = d3.layout.chord()
  .padding(.03)
  .sortSubgroups(d3.descending)
  .matrix(data);

  // Update arcs

  svg.selectAll(".group")
  .data(layout.groups)
  .transition()
  .duration(1500)
  .attrTween("d", arcTween(last_chord));

  // Update chords

  svg.select(".chord")
     .selectAll("path")
     .data(layout.chords)
     .transition()
     .duration(1500)
     .attrTween("d", chordTween(last_chord))

};

var arc =  d3.svg.arc()
      .startAngle(function(d) { return d.startAngle })
      .endAngle(function(d) { return d.endAngle })
      .innerRadius(r0)
      .outerRadius(r1);

var chordl = d3.svg.chord().radius(r0);

function arcTween(layout) {
  return function(d,i) {
    var i = d3.interpolate(layout.groups()[i], d);

    return function(t) {
      return arc(i(t));
    }
  }
}

function chordTween(layout) {
  return function(d,i) {
    var i = d3.interpolate(layout.chords()[i], d);

    return function(t) {
      return chordl(i(t));
    }
  }
}

1 ответ

Решение

Создание диаграммы аккордов

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

Sample Chord Diagram, from the example linked above

Во-первых, аспект манипулирования данными. Утилита d3 Chord Layout берет ваши данные о взаимодействиях между различными группами и создает набор объектов данных, которые содержат исходные данные, но которым также назначены измерения углов. Таким образом, он похож на инструмент круговой разметки, но есть некоторые важные различия, связанные с повышенной сложностью разметки аккордов.

Как и другие инструменты макета d3, вы создаете объект макета аккордов, вызывая функцию (d3.layout.chord()), а затем вы вызываете дополнительные методы объекта макета, чтобы изменить настройки по умолчанию. В отличие от инструмента круговой разметки и большинства других разметок, объект разметки аккордов не является функцией, которая принимает ваши данные в качестве входных данных и выводит вычисленный массив объектов данных с установленными атрибутами (углами) разметки.

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

var chordLayout = d3.layout.chord() //create layout object
                  .sortChords( d3.ascending ) //set a property
                  .padding( 0.01 ); //property-setting methods can be chained

chordLayout.matrix( data );  //set the data matrix

Доступ к объектам данных группы осуществляется путем вызова .groups() на макете аккордов после того, как матрица данных была установлена. Каждая группа эквивалентна строке в вашей матрице данных (т. Е. Каждый подмассив в массиве массивов). Групповым объектам данных были назначены значения начального угла и конечного угла, представляющие сечение круга. Это очень похоже на круговую диаграмму, с той разницей, что значения для каждой группы (и для круга в целом) рассчитываются путем суммирования значений для всей строки (подрешетки). Объекты групповых данных также имеют свойства, представляющие их индекс в исходной матрице (важно, потому что они могут быть отсортированы в другом порядке) и их общее значение.

Доступ к объектам данных аккордов осуществляется путем вызова .chords() на макете аккордов после того, как матрица данных была установлена. Каждый аккорд представляет два значения в матрице данных, эквивалентных двум возможным отношениям между двумя группами. Например, в примере @latortue09 отношения представляют собой поездки на велосипеде между окрестностями, поэтому аккорд, который представляет поездки между Соседством A и Соседством B, представляет количество поездок от A до B, а также число от B до A. Если Соседство A в ряду a вашей матрицы данных и соседства B в строке b то эти значения должны быть data[a][b] а также data[b][a] соответственно. (Конечно, иногда отношения, которые вы рисуете, не имеют такого типа направления, в этом случае ваша матрица данных должна быть симметричной, что означает, что эти два значения должны быть равными.)

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

Именование источника / цели несколько сбивает с толку, поскольку, как я упоминал выше, объект chord представляет оба направления отношений между двумя группами. Направление, которое имеет большее значение, определяет, какая группа называется source и который называется target, Таким образом, если есть 200 рейсов из Соседства A в Соседство B, но 500 рейсов из B в A, то source для этого аккорда объект будет представлять часть сегмента окружности B круга, а target будет представлять часть сегмента соседства А круга. Для отношений между группой и самой собой (в этом примере поездки, которые начинаются и заканчиваются в одной и той же окрестности), исходный и целевой объекты совпадают.

Последний важный аспект массива объектов аккордов состоит в том, что он содержит объекты только там, где существуют отношения между двумя группами. Если между Соседством A и Соседством B в обоих направлениях нет поездок, то для этих групп не будет объекта данных аккордов. Это становится важным при обновлении от одного набора данных к другому.

Во-вторых, аспект визуализации данных. Инструмент Chord Layout создает массивы объектов данных, преобразуя информацию из матрицы данных в углы круга. Но это ничего не привлекает. Чтобы создать стандартное SVG-представление диаграммы аккордов, вы используете выбор d3 для создания элементов, объединенных в массив объектов данных макета. Поскольку на диаграмме аккордов есть два разных массива объектов данных макета, один для аккордов и один для групп, существует два разных варианта d3.

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

Форма <path> определяется его "d" (данные пути или направления). D3 имеет множество генераторов данных пути, которые являются функциями, которые принимают объект данных и создают строку, которая может использоваться для пути "d" приписывать. Каждый генератор пути создается путем вызова метода d3, и каждый может быть изменен путем вызова своих собственных методов.

Группы в стандартной диаграмме аккордов нарисованы с использованием d3.svg.arc() генератор данных пути. Этот генератор дуг является тем же самым, который используется для круговых и кольцевых графиков. В конце концов, если вы удалите аккорды из диаграммы аккордов, вы по сути просто получите кольцевую диаграмму, составленную из дуг группы. Генератор дуг по умолчанию ожидает передачи объектов данных с startAngle а также endAngle свойства; объекты данных группы, созданные макетом аккордов, работают с этим значением по умолчанию. Генератор дуги также должен знать внутренний и внешний радиус дуги. Они могут быть указаны как функции данных или как константы; для диаграммы аккордов они будут постоянными, одинаковыми для каждой дуги.

var arcFunction = d3.svg.arc() //create the arc path generator
                               //with default angle accessors
                  .innerRadius( radius )
                  .outerRadius( radius + bandWidth); 
                               //set constant radius values

var groupPaths = d3.selectAll("path.group")
                 .data( chordLayout.groups() ); 
    //join the selection to the appropriate data object array 
    //from the chord layout 

groupPaths.enter().append("path") //create paths if this isn't an update
          .attr("class", "group"); //set the class
          /* also set any other attributes that are independent of the data */

groupPaths.attr("fill", groupColourFunction )
          //set attributes that are functions of the data
          .attr("d", arcFunction ); //create the shape
   //d3 will pass the data object for each path to the arcFunction
   //which will create the string for the path "d" attribute

Аккорды в диаграмме аккордов имеют форму, уникальную для этого типа диаграммы. Их формы определяются с помощью d3.svg.chord() генератор данных пути. Генератор аккордов по умолчанию ожидает данные формы, созданной объектом макета аккордов, единственное, что необходимо указать, - это радиус круга (который обычно будет таким же, как внутренний радиус групп дуг).

var chordFunction = d3.svg.chord() //create the chord path generator
                                   //with default accessors
                    .radius( radius );  //set constant radius

var chordPaths = d3.selectAll("path.chord")
                 .data( chordLayout.chords() ); 
    //join the selection to the appropriate data object array 
    //from the chord layout 

chordPaths.enter().append("path") //create paths if this isn't an update
          .attr("class", "chord"); //set the class
          /* also set any other attributes that are independent of the data */

chordPaths.attr("fill", chordColourFunction )
          //set attributes that are functions of the data
          .attr("d", chordFunction ); //create the shape
   //d3 will pass the data object for each path to the chordFunction
   //which will create the string for the path "d" attribute

Это простой случай, с <path> только элементы. Если вы также хотите, чтобы текстовые метки были связаны с вашими группами или аккордами, то ваши данные объединяются в <g> элементы, а <path> элементы и <text> элементы для меток (и любые другие элементы, такие как линии отметок в примере цвета волос) являются потомками объекта, который наследует его объект данных. При обновлении графика вам нужно будет обновить все подкомпоненты, на которые влияют данные.

Обновление диаграммы аккордов

Имея в виду всю эту информацию, как вам следует подходить к созданию диаграммы аккордов, которую можно обновлять новыми данными?

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

Для этого примера шаги инициализации будут включать создание <svg> и по центру <g> элемент, а также чтение в массиве информации о различных окрестностях. Затем метод инициализации вызовет метод обновления с матрицей данных по умолчанию. Кнопки, которые переключаются на другую матрицу данных, будут вызывать один и тот же метод.

/*** Initialize the visualization ***/
var g = d3.select("#chart_placeholder").append("svg")
        .attr("width", width)
        .attr("height", height)
    .append("g")
        .attr("id", "circle")
        .attr("transform", 
              "translate(" + width / 2 + "," + height / 2 + ")");
//the entire graphic will be drawn within this <g> element,
//so all coordinates will be relative to the center of the circle

g.append("circle")
    .attr("r", outerRadius);

d3.csv("data/neighborhoods.csv", function(error, neighborhoodData) {

    if (error) {alert("Error reading file: ", error.statusText); return; }

    neighborhoods = neighborhoodData; 
        //store in variable accessible by other functions
    updateChords(dataset); 
    //call the update method with the default dataset url

} ); //end of d3.csv function

/* example of an update trigger */
d3.select("#MenOnlyButton").on("click", function() {
    updateChords( "/data/men_trips.json" );
    disableButton(this);
});

Я просто передаю URL-адрес данных в функцию обновления, что означает, что первой строкой этой функции будет вызов функции синтаксического анализа данных. Полученная матрица данных используется в качестве матрицы для нового объекта макета данных. Нам нужен новый объект макета, чтобы сохранить копию старого макета для функций перехода. (Если вы не собираетесь переносить изменения, вы можете просто позвонить matrix метод на том же макете для создания нового.) Чтобы минимизировать дублирование кода, я использую функцию для создания нового объекта макета и установки всех его параметров:

/* Create OR update a chord layout from a data matrix */
function updateChords( datasetURL ) {

  d3.json(datasetURL, function(error, matrix) {

    if (error) {alert("Error reading file: ", error.statusText); return; }

    /* Compute chord layout. */
    layout = getDefaultLayout(); //create a new layout object
    layout.matrix(matrix);

    /* main part of update method goes here */

  }); //end of d3.json
}

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

Во-первых, цепочка объединения данных. Один для групп и один для аккордов.
Чтобы поддерживать постоянство объектов с помощью переходов и уменьшить количество графических свойств, которые вы должны установить при обновлении, вы захотите установить ключевую функцию в вашем объединении данных. По умолчанию d3 сопоставляет данные с элементами в пределах выбора, основываясь только на их порядке в странице / массиве. Потому что наш макет аккордов .chords() Массив не содержит аккорды, если в этом наборе данных нет нулевых отношений, порядок аккордов может быть несовместимым между раундами обновления. .groups() Массив также можно пересортировать по порядкам, которые не соответствуют исходной матрице данных, поэтому мы также добавим ключевую функцию, чтобы это было безопасно. В обоих случаях ключевые функции основаны на .index свойства, которые макет аккордов хранит в объектах данных.

/* Create/update "group" elements */
var groupG = g.selectAll("g.group")
    .data(layout.groups(), function (d) {
        return d.index; 
        //use a key function in case the 
        //groups are sorted differently between updates
    });

/* Create/update the chord paths */
var chordPaths = g.selectAll("path.chord")
    .data(layout.chords(), chordKey );
        //specify a key function to match chords
        //between updates

/* Elsewhere, chordKey is defined as: */

function chordKey(data) {
    return (data.source.index < data.target.index) ?
        data.source.index  + "-" + data.target.index:
        data.target.index  + "-" + data.source.index;

    //create a key that will represent the relationship
    //between these two groups *regardless*
    //of which group is called 'source' and which 'target'
}

Обратите внимание, что аккорды <path> элементы, но группы <g> элементы, которые будут содержать как <path> и <text>,

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

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

var newGroups = groupG.enter().append("g")
    .attr("class", "group");
//the enter selection is stored in a variable so we can
//enter the <path>, <text>, and <title> elements as well

//Create the title tooltip for the new groups
newGroups.append("title");

//create the arc paths and set the constant attributes
//(those based on the group index, not on the value)
newGroups.append("path")
    .attr("id", function (d) {
        return "group" + d.index;
        //using d.index and not i to maintain consistency
        //even if groups are sorted
    })
    .style("fill", function (d) {
        return neighborhoods[d.index].color;
    });

//create the group labels
newGroups.append("svg:text")
    .attr("dy", ".35em")
    .attr("color", "#fff")
    .text(function (d) {
        return neighborhoods[d.index].name;
    });


//create the new chord paths
var newChords = chordPaths.enter()
    .append("path")
    .attr("class", "chord");

// Add title tooltip for each new chord.
newChords.append("title");

Обратите внимание, что цвета ввода для групповых дуг задаются при вводе, но не цвета заливки для аккордов. Это связано с тем, что цвет аккорда будет меняться в зависимости от того, какая группа (из двух соединяемых аккордов) называется "источником", а какая "целевой", т. Е. В зависимости от того, какое направление отношений сильнее (имеет больше поездок).

В-третьих, цепочка обновлений. Когда вы добавляете элемент к .enter() Выделение, этот новый элемент заменяет пустое место в исходной выборке соединения данных. После этого, если вы манипулируете исходным выделением, настройки применяются как к новым, так и к обновляющим элементам. Так что здесь вы можете установить любые свойства, которые зависят от данных.

//Update the (tooltip) title text based on the data
groupG.select("title")
    .text(function(d, i) {
        return numberWithCommas(d.value) 
            + " trips started in " 
            + neighborhoods[i].name;
    });

//update the paths to match the layout
groupG.select("path") 
    .transition()
        .duration(1500)
        .attr("opacity", 0.5) //optional, just to observe the transition
    .attrTween("d", arcTween( last_layout ) )
        .transition().duration(10).attr("opacity", 1) //reset opacity
    ;

//position group labels to match layout
groupG.select("text")
    .transition()
        .duration(1500)
        .attr("transform", function(d) {
            d.angle = (d.startAngle + d.endAngle) / 2;
            //store the midpoint angle in the data object

            return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" +
                " translate(" + (innerRadius + 26) + ")" + 
                (d.angle > Math.PI ? " rotate(180)" : " rotate(0)"); 
            //include the rotate zero so that transforms can be interpolated
        })
        .attr("text-anchor", function (d) {
            return d.angle > Math.PI ? "end" : "begin";
        });

// Update all chord title texts
chordPaths.select("title")
    .text(function(d) {
        if (neighborhoods[d.target.index].name !== 
                neighborhoods[d.source.index].name) {

            return [numberWithCommas(d.source.value),
                    " trips from ",
                    neighborhoods[d.source.index].name,
                    " to ",
                    neighborhoods[d.target.index].name,
                    "\n",
                    numberWithCommas(d.target.value),
                    " trips from ",
                    neighborhoods[d.target.index].name,
                    " to ",
                    neighborhoods[d.source.index].name
                    ].join(""); 
                //joining an array of many strings is faster than
                //repeated calls to the '+' operator, 
                //and makes for neater code!
        } 
        else { //source and target are the same
            return numberWithCommas(d.source.value) 
                + " trips started and ended in " 
                + neighborhoods[d.source.index].name;
        }
    });

//update the path shape
chordPaths.transition()
    .duration(1500)
    .attr("opacity", 0.5) //optional, just to observe the transition
    .style("fill", function (d) {
        return neighborhoods[d.source.index].color;
    })
    .attrTween("d", chordTween(last_layout))
    .transition().duration(10).attr("opacity", 1) //reset opacity
;

//add the mouseover/fade out behaviour to the groups
//this is reset on every update, so it will use the latest
//chordPaths selection
groupG.on("mouseover", function(d) {
    chordPaths.classed("fade", function (p) {
        //returns true if *neither* the source or target of the chord
        //matches the group that has been moused-over
        return ((p.source.index != d.index) && (p.target.index != d.index));
    });
});
//the "unfade" is handled with CSS :hover class on g#circle
//you could also do it using a mouseout event on the g#circle

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

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

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

//handle exiting groups, if any, and all their sub-components:
groupG.exit()
    .transition()
        .duration(1500)
        .attr("opacity", 0)
        .remove(); //remove after transitions are complete


//handle exiting paths:
chordPaths.exit().transition()
    .duration(1500)
    .attr("opacity", 0)
    .remove();

О пользовательских функциях анимации:

Если вы просто использовали анимацию по умолчанию для переключения с одной формы пути на другую, результаты могут выглядеть несколько странно. Попробуйте переключиться с "Только для мужчин" на "Только для женщин", и вы увидите, что аккорды отсоединяются от края круга. Если бы позиции дуг изменились более значительно, вы бы увидели, что они пересекают круг, чтобы достичь своего нового положения, а не скользят по кольцу.

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

Пользовательская функция анимации позволяет вам определять, как должен формироваться путь на каждом шаге перехода. Я написал комментарии о функциях анимации здесь и здесь, поэтому я не буду перефразировать это. Но краткое описание состоит в том, что функция анимации, которую вы передаете .attrTween(attribute, tween) должна быть функцией, которая вызывается один раз для каждого элемента, и должна сама возвращать функцию, которая будет вызываться при каждом "тике" перехода, чтобы вернуть значение атрибута в этой точке перехода.

Чтобы получить плавные переходы форм траектории, мы используем две функции генератора данных траектории - генератор дуги и генератор аккордов - для создания данных траектории на каждом шаге перехода. Таким образом, дуги всегда будут выглядеть как дуги, а аккорды всегда будут выглядеть как аккорды. Переходная часть является начальным и конечным значениями угла. Используя два разных объекта данных, которые описывают один и тот же тип фигуры, но с разными значениями углов, вы можете использовать d3.interpolateObject(a,b) создать функцию, которая даст вам объект на каждой стадии перехода с соответствующими свойствами угла. Таким образом, если у вас есть объект данных из старого макета и соответствующий объект данных из нового макета, вы можете плавно сместить дуги или аккорды из одной позиции в другую.

Однако что делать, если у вас нет старого объекта данных? Либо потому, что у этого аккорда не было совпадения в старом макете, либо потому, что визуализация визуализируется впервые, а старого макета нет. Если вы передаете пустой объект в качестве первого параметра d3.interpolateObject, переходный объект всегда будет точно конечным значением. В сочетании с другими переходами, такими как непрозрачность, это может быть приемлемым. Однако я решил сделать переход таким, чтобы он начинался с формы нулевой ширины, то есть формы, в которой начальные углы совпадали с конечными углами, а затем расширялся до конечной формы:

function chordTween(oldLayout) {
    //this function will be called once per update cycle

    //Create a key:value version of the old layout's chords array
    //so we can easily find the matching chord 
    //(which may not have a matching index)

    var oldChords = {};

    if (oldLayout) {
        oldLayout.chords().forEach( function(chordData) {
            oldChords[ chordKey(chordData) ] = chordData;
        });
    }

    return function (d, i) {
        //this function will be called for each active chord

        var tween;
        var old = oldChords[ chordKey(d) ];
        if (old) {
            //old is not undefined, i.e.
            //there is a matching old chord value

            //check whether source and target have been switched:
            if (d.source.index != old.source.index ){
                //swap source and target to match the new data
                old = {
                    source: old.target,
                    target: old.source
                };
            }

            tween = d3.interpolate(old, d);
        }
        else {
            //create a zero-width chord object
            var emptyChord = {
                source: { startAngle: d.source.startAngle,
                         endAngle: d.source.startAngle},
                target: { startAngle: d.target.startAngle,
                         endAngle: d.target.startAngle}
            };
            tween = d3.interpolate( emptyChord, d );
        }

        return function (t) {
            //this function calculates the intermediary shapes
            return path(tween(t));
        };
    };
}

(Проверьте скрипку для кода анимации дуги, который немного проще)

Живая версия в целом: http://jsfiddle.net/KjrGF/12/

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