Комплексная обработка d3.nest()

У меня есть массив массивов, который выглядит так:

var arrays = [[1,2,3,4,5],
              [1,2,6,4,5],
              [1,3,6,4,5],
              [1,2,3,6,5],
              [1,7,5],
              [1,7,3,5]]

Я хочу использовать d3.nest() или даже просто стандартный javascript для преобразования этих данных во вложенную структуру данных, которую я могу использовать с d3.partition.

В частности, я хочу создать этот flare.json данных flare.json.

Уровни объекта json, которые я хочу создать с помощью d3.nest() соответствуют позициям индекса в массиве. Обратите внимание, что 1 находится в первой позиции во всех подмассивах в приведенных выше примерах данных; следовательно, это в корне дерева. На следующих позициях в массивах есть три значения, 2, 3 и 7, поэтому корневое значение 1 имеет 3 дочерних элемента. На данный момент дерево выглядит так:

      1
    / | \
   2  3  7

На третьей позиции в подмассивах находятся четыре значения, 3, 5 и 6. Эти дети будут места в дереве следующим образом:

            1
        ____|___
       /    |    \
      2     3     7
     / \   /     / \
    3   6 6     3   5

Как я могу создать эту структуру данных, используя d3.nest()? Полная структура данных с примерами данных, которые я показал выше, должна выглядеть следующим образом:

   {"label": 1, 
     "children": [
        {"label": 2, "children": [
            {"label": 3, "children": [
                {"label": 4, "children": [
                    {"label": 5}
                ]},
                {"label": 6, "children": [
                    {"label": 5}
                ]}
            ]},
            {"label": 6, "children": [
                {"label": 4, "children": [
                    {"label": 5}
                ]}
            ]},
        {"label": 3, "children": [
            {"label": 6, "children": [
                {"label": 4, "children": [
                    {"label": 5}
                ]}
            ]}
        ]},
        {"label": 7, "children": [
            {"label": 3, "children": [
                {"label": 5}
            ]},
            {"label": 5}
        ]}
      ]}
    ]}

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

var data = d3.nest()
  .key(function(d, i) { return d.i; })
  .rollup(function(d) { return d.length; })

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

Ответ @meetamit в комментариях хорош, но в моем случае мое дерево слишком глубокое, чтобы неоднократно применять .keys() к данным, поэтому я не могу вручную написать такую функцию.

Ответ 1

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

Чтобы облегчить поиск дочернего элемента с заданным ярлыком, я реализовал children как объект данных/ассоциативный массив вместо пронумерованного массива. Если вы хотите быть действительно надежным, вы можете использовать d3.map по причинам, описанным в этой ссылке, но если ваши метки на самом деле целые, чем это не будет проблема. В любом случае, это просто означает, что когда вам нужно получить доступ к дочерним элементам в виде массива (например, для функций макета d3), вам нужно указать функцию, чтобы сделать массив из значений объекта - d3.values(object) функция утилиты делает это для вас.

key code:

var root={}, 
    path, node, next, i,j, N, M;

for (i = 0, N=arrays.length; i<N; i++){
    //for each path in the data array 
    path = arrays[i];
    node = root; //start the path from the root

    for (j=0,M=path.length; j<M; j++){
        //follow the path through the tree
        //creating new nodes as necessary

        if (!node.children){ 
            //undefined, so create it:
            node.children = {}; 
        //children is defined as an object 
        //(not array) to allow named keys
        }

        next = node.children[path[j]];
        //find the child node whose key matches
        //the label of this step in the path

        if (!next) {
            //undefined, so create
            next = node.children[path[j]] = 
                {label:path[j]};
        }
        node = next; 
        // step down the tree before analyzing the
        // next step in the path.        
    }    
}

Реализовано с помощью массива данных выборки и базового метода диаграммного дендрограммы :
http://fiddle.jshell.net/KWc73/

Отредактировано для добавления: Как упоминалось в комментариях, чтобы получить результат, выглядящий точно в соответствии с запросом:

  • Доступ к корневому объекту данных из дочернего массива корневых объектов по умолчанию.
  • Используйте рекурсивную функцию для циклического перехода по дереву, заменяя дочерние объекты дочерними массивами.

Вот так:

root = d3.values(root.children)[0];
//this is the root from the original data, 
//assuming all paths start from one root, like in the example data

//recurse through the tree, turning the child
//objects into arrays
function childrenToArray(n){
    if (n.children) {
        //this node has children

        n.children = d3.values(n.children);
        //convert to array

        n.children.forEach(childrenToArray);
        //recurse down tree
    }
}
childrenToArray(root);

Обновленная скрипка:
http://fiddle.jshell.net/KWc73/1/

Ответ 2

Если вы расширите спецификацию Array, это не будет на самом деле сложным. Основная идея состоит в том, чтобы создать уровень дерева по уровню, каждый элемент массива за раз и по сравнению с предыдущим. Это код (минус расширения):

function process(prevs, i) {
  var vals = arrays.filter(function(d) { return prevs === null || d.slice(0, i).compare(prevs); })
                 .map(function(d) { return d[i]; }).getUnique();
  return vals.map(function(d) {
    var ret = { label: d }
    if(i < arrays.map(function(d) { return d.length; }).max() - 1) {
        tmp = process(prevs === null ? [d] : prevs.concat([d]), i+1);
        if(tmp.filter(function(d) { return d.label != undefined; }).length > 0)
          ret.children = tmp;
    }
    return ret;
  });
}

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

Завершите jsfiddle здесь.

Несколько более подробных объяснений:

  • Сначала мы получаем массивы, релевантные для текущего пути. Это делается с помощью filter вывода тех, которые не совпадают с prevs, который является нашим текущим (частичным) путем. В начале prevs имеет значение null и ничего не фильтруется.
  • Для этих массивов мы получаем значения, соответствующие текущему уровню в дереве (элемент i th). Дубликаты фильтруются. Это делается с помощью .map() и .getUnique().
  • Для каждого из значений, которые мы получили таким образом, будет возвращено значение. Поэтому мы перебираем их (vals.map()). Для каждого мы устанавливаем атрибут label. Остальная часть кода определяет, есть ли дети и получает их через рекурсивный вызов. Для этого сначала проверяем, остались ли элементы в массивах, т.е. Если мы находимся на самом глубоком уровне дерева. Если это так, мы делаем рекурсивный вызов, передавая новый prev, который включает в себя элемент, который мы сейчас обрабатываем, и следующий уровень (i+1). Наконец, мы проверяем результат этого рекурсивного вызова для пустых элементов - если есть только пустые дочерние элементы, мы их не сохраняем. Это необходимо, потому что не все массивы (т.е. Не все пути) имеют одинаковую длину.

Ответ 3

Поскольку d3-collection устарела в пользу d3.array, мы можем использовать d3.groups для достижения того, что раньше работало с d3.nest:

var input = [
  [1, 2, 3, 4, 5],
  [1, 2, 6, 4, 5],
  [1, 3, 6, 4, 5],
  [1, 2, 3, 6, 5],
  [1, 7, 5],
  [1, 7, 3, 5]
];

function process(arrays, depth) {
  return d3.groups(arrays, d => d[depth]).map(x => {
    if (
      x[1].length > 1 ||                     // if there is more than 1 child
      (x[1].length == 1 && x[1][0][depth+1]) // if there is 1 child and the future depth is inferior to the child length
    )
      return ({
        "label": x[0],
        "children": process(x[1], depth+1)
      });
    return ({ "label": x[0] });              // if there is no child
  });
};

console.log(process(input, 0));
<script src="https://d3js.org/d3-array.v2.min.js"></script>