Комплексная обработка 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>