Построение графа объектов из плоского массива объектов в javascript

у меня есть массив объектов javascript с объектами, которые выглядят так:

itemId
name
parentItemId ‹== элементы верхнего уровня без родителя имеют нулевое значение

Я хочу построить график, в котором родительские элементы содержат массивы дочерних элементов, а эти дочерние элементы имеют массивы дочерних элементов, если это применимо.

Каков хороший способ сделать это?


person billy jean    schedule 13.09.2011    source источник
comment
вы можете попробовать такой плагин, как jqplot.com   -  person Joseph Marikle    schedule 14.09.2011
comment
Привет Джозеф, интересный проект! но я не имел в виду диаграмму/график.. просто граф вложенных объектов..   -  person billy jean    schedule 14.09.2011


Ответы (3)


function objectGraph(items)
{
    var items_by_id = {};
    var roots = [];
    var i;

    // Build an id->object mapping, so we don't have to go hunting for parents
    for (i = 0; i < items.length; ++i) {
        items_by_id[items[i].itemId] = items[i];
        items[i].children = [];
    }

    for (i = 0; i < items.length; ++i) {
        var parentId = items[i].parentItemId;
        // If parentId is null, this is a root; otherwise, it's parentId's kid
        var nodes = (parentId === null) ? roots : items_by_id[parentId].children;
        nodes.push(items[i]);
    }
    return roots;
}

Обратите внимание, этот код дает каждому узлу свойство children, которое пусто, если у узла нет дочерних элементов. Лично я нахожу это более простым и последовательным, чем каждый узел может иметь или не иметь детей; вы можете перебирать children, не беспокоясь о том, существует ли он. Листовой узел будет иметь children.length == 0.

Если вы можете гарантировать, что у вас ровно один корень, вы можете return roots[0]; вместо возврата массива.

person cHao    schedule 13.09.2011
comment
поэтому вместо того, чтобы делать if(children), вы делаете if(children.length == 0)... хм, я думаю, это имеет смысл, если вы хотите выполнить итерацию элементов, а затем решить, является ли это листом или нет. Я обычно делаю наоборот. - person James; 14.09.2011
comment
Вместо того, чтобы делать if (children), вы бы сделали if (children.length). Любой из способов может работать нормально (проверка существования списка по сравнению с тем, содержит ли он что-либо), но последний означает меньше специального регистра и меньше беспокойства о том, получите ли вы ошибки неопределенного объекта. Если вы хотите, вы можете обращаться с листовым узлом, как с любым другим, и перебирать его дочерние узлы; у вас просто не будет детей, с которыми можно было бы что-то сделать. :) Обратите внимание, что DOM работает аналогично; каждый узел имеет список дочерних элементов, даже если этот список пуст. - person cHao; 14.09.2011
comment
Достаточно верно, а также firefox (вероятно, все браузеры) возвращают 0 для children.length пустого элемента DOM. Также мое решение делает то же самое, лол. - person James; 14.09.2011
comment
+1 Это решение подходит, спасибо за урок cHao. Теперь у меня есть несколько изменений, которые нужно внести в мой собственный код :) - person James; 14.09.2011
comment
Я дам свой реквизит здесь к этому. Он только дважды перебирает весь массив, тогда как рекурсивные параметры, которые мы придумали, будут перебирать весь массив при каждом рекурсивном вызове. Умная работа! Я бы предложил кэшировать свойство .length массива в переменной, так как в некоторых браузерах это может ускорить цикл. - person ericb; 14.09.2011

Когда вы строите «функцию построения дерева», вы должны решить, является ли «вещь верхнего уровня» одним элементом или массивом элементов. Поскольку вы сказали itemS, мы используем массив. Разница заключается в параметре, который вы передаете и получаете обратно, если это массив, мы передаем parentId, в противном случае мы передаем идентификатор.

function buildTree(parentId, list) {
  var nodes = [];
  for (var i=0, l; l = list[i]; i++) {
    if (l.parentId === parentId) {
// if you need "myList" intact afterwards remove the next line at the cost of efficiency
      list.splice(i, 1); i--;  
      nodes.push({
        id: l.id
        ,parentId: l.parentId
        ,name: l.name
        ,children: buildTree(l.id, list)
      });
    }
  }
  return nodes;
}

var myTree = buildTree(null, myList);
person James    schedule 13.09.2011
comment
Единственное серьезное замечание, которое у меня есть по поводу рекурсивного решения (или, скорее, рекурсивных решений, которые я видел и о которых думал), заключается в том, что оно будет безумно медленным для большого количества узлов. Каждый некорневой узел потребует сканирования всего массива для своих дочерних элементов, что делает алгоритм O (n ^ 2) - или хуже, если только толчки массива не выполняются с постоянным временем. Это концептуально проще, но это не имеет большого значения, если для запуска требуется день. :) - person cHao; 14.09.2011
comment
Исправлено - удалять элементы по мере их нахождения - стало лучше, но все равно не супербыстро. - person James; 14.09.2011
comment
К сожалению, объединение массива AFAIK - это операция O (n). Для действительно больших массивов это может фактически замедлить его, так как теперь вы потенциально находитесь в O (n ^ 3). - person cHao; 14.09.2011

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

Обратите внимание, что это будет работать для N-уровневых дочерних объектов, а не только для одного уровня.

var finalArray = [];
var YOUR_RAW_ARRAY = [];

var buildObjectGraph = function(inputArray){
    var i = 0, len = inputArray.length;
    var returnVal = [];

    for(;i<len;i++){
        if(inputArray[i].parentItemId === null){
            findChildren(inputArray[i], inputArray);
            returnVal.push(inputArray[i]);
        }
    }

    var findChildren = function(root){
        var i = 0, i2 = 0, len = rawDataArray.length, len2 = 0;

        for(;i<len;i++){
            if(inputArray[i].parentItemId === root.itemId){
                if(root.children){
                    root.children.push(inputArray[i]);
                }else{
                    root.children = [];
                    root.children.push(inputArray[i]);
                }
            }
        }

        //now call it recursively
        len2 = root.children.length;
        if(len2 > 0){
            for(;i2 < len2; i2++){
                findChildren(root.children[i2]);
            }
        }
    };
    return returnVal;
};

//Then execute it
finalArray = buildObjectGraph(YOUR_RAW_ARRAY); 
person ericb    schedule 13.09.2011
comment
эй большое спасибо! есть ли какой-то способ оптимизации, удалив элементы в массиве RAW, которые уже были отправлены? - person billy jean; 14.09.2011
comment
Я действительно рассматривал попытку splice извлекать части входного массива во время обработки, но, поскольку обе функции используют указатели обратно на inputArray, это действительно испортит итератор, пытающийся перебрать каждый элемент. Кроме того, глядя на производительность объединения разных массивов для передачи функции findChildren, вам может быть не лучше, поскольку это может иметь некоторые накладные расходы. Наконец, проверка свойства .length массива немного замедляет работу (особенно для больших массивов). Гораздо лучше сначала кэшировать его в переменной, а затем зацикливаться на ней. - person ericb; 14.09.2011